skills/admin/batch-operations/SKILL.md
Efficient bulk operations for mass record creation, updates with relationships, performance optimization, and error handling in batch processing
npx skillsauth add happy-technologies-llc/happy-servicenow-skills batch-operationsInstall 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 efficient techniques for bulk operations in ServiceNow:
When to use: When creating or updating many records at once, data migrations, bulk imports, or mass data corrections.
Who should use this: Administrators, developers, and data migration specialists.
admin or table-specific write permissions| Volume | Method | Estimated Time | |--------|--------|----------------| | 1-10 | Parallel MCP calls | < 5 seconds | | 10-100 | SN-Batch-Create/Update | 5-30 seconds | | 100-1,000 | Background script batches | 1-5 minutes | | 1,000-10,000 | Scheduled job with batching | 10-60 minutes | | 10,000+ | Import sets or data pump | Hours |
Tested and Verified: 43+ parallel MCP calls successful in single message.
Parallel MCP calls are preferred for:
- Independent record operations
- Different tables
- No interdependencies
- Maximum throughput
Create multiple records in a single operation.
Using MCP:
Tool: SN-Batch-Create
Parameters:
records:
- table_name: incident
data:
short_description: "Server down - Web01"
caller_id: [user_sys_id]
category: hardware
priority: 1
- table_name: incident
data:
short_description: "Server down - Web02"
caller_id: [user_sys_id]
category: hardware
priority: 1
- table_name: incident
data:
short_description: "Server down - Web03"
caller_id: [user_sys_id]
category: hardware
priority: 1
Response:
{
"success": true,
"created": [
{ "table": "incident", "sys_id": "abc123...", "number": "INC0010001" },
{ "table": "incident", "sys_id": "def456...", "number": "INC0010002" },
{ "table": "incident", "sys_id": "ghi789...", "number": "INC0010003" }
],
"errors": []
}
For maximum throughput, use parallel MCP calls in a single message.
Parallel Incident Creation (10 records):
# Call 1:
Tool: SN-Create-Record
Parameters:
table_name: incident
data:
short_description: "Batch test 1"
category: inquiry
# Call 2:
Tool: SN-Create-Record
Parameters:
table_name: incident
data:
short_description: "Batch test 2"
category: inquiry
# Call 3-10: Continue pattern...
All calls execute simultaneously - no waiting between operations.
Create parent and child records maintaining relationships.
Using MCP (Sequential for Dependencies):
# Step 1: Create parent record
Tool: SN-Create-Record
Parameters:
table_name: change_request
data:
short_description: "Server Infrastructure Upgrade"
category: hardware
type: standard
# Step 2: Create child tasks (use parent sys_id from step 1)
Tool: SN-Batch-Create
Parameters:
records:
- table_name: change_task
data:
parent: [change_request_sys_id]
short_description: "Backup existing configuration"
change_task_type: planning
order: 100
- table_name: change_task
data:
parent: [change_request_sys_id]
short_description: "Install new hardware"
change_task_type: implementation
order: 200
- table_name: change_task
data:
parent: [change_request_sys_id]
short_description: "Verify functionality"
change_task_type: testing
order: 300
For complex logic or very large batches:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Batch creation with relationships
var projectData = [
{
name: 'Project Alpha',
tasks: [
{ name: 'Task A1', order: 1 },
{ name: 'Task A2', order: 2 },
{ name: 'Task A3', order: 3 }
]
},
{
name: 'Project Beta',
tasks: [
{ name: 'Task B1', order: 1 },
{ name: 'Task B2', order: 2 }
]
}
];
var created = { projects: 0, tasks: 0 };
projectData.forEach(function(project) {
// Create project
var prj = new GlideRecord('pm_project');
prj.initialize();
prj.short_description = project.name;
var prjId = prj.insert();
created.projects++;
// Create tasks
project.tasks.forEach(function(task) {
var tsk = new GlideRecord('pm_project_task');
tsk.initialize();
tsk.parent = prjId;
tsk.short_description = task.name;
tsk.order = task.order;
tsk.insert();
created.tasks++;
});
gs.info('Created project: ' + project.name + ' with ' + project.tasks.length + ' tasks');
});
gs.info('Summary: ' + JSON.stringify(created));
description: Batch create projects with tasks
Update multiple records by sys_id.
Using MCP:
Tool: SN-Batch-Update
Parameters:
updates:
- table_name: incident
sys_id: [incident1_sys_id]
data:
priority: 2
state: 2
work_notes: "Escalated per management request"
- table_name: incident
sys_id: [incident2_sys_id]
data:
priority: 2
state: 2
work_notes: "Escalated per management request"
- table_name: incident
sys_id: [incident3_sys_id]
data:
priority: 2
state: 2
work_notes: "Escalated per management request"
Update all records matching a query.
Using Background Script:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Query-based bulk update
var TABLE = 'incident';
var QUERY = 'active=true^category=inquiry^priority=5';
var UPDATES = {
priority: 4,
work_notes: 'Priority adjusted per SLA review'
};
var MAX_UPDATES = 1000;
var DRY_RUN = true;
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.setLimit(MAX_UPDATES);
gr.query();
gs.info('Found ' + gr.getRowCount() + ' records matching query');
var updated = 0;
var errors = 0;
while (gr.next()) {
try {
if (DRY_RUN) {
gs.info('[DRY RUN] Would update: ' + gr.number);
} else {
for (var field in UPDATES) {
gr[field] = UPDATES[field];
}
gr.update();
updated++;
}
} catch (e) {
errors++;
gs.error('Error updating ' + gr.number + ': ' + e.message);
}
}
gs.info('Complete: Updated=' + updated + ', Errors=' + errors + ', DryRun=' + DRY_RUN);
description: Bulk update incidents by query
Apply different updates based on record conditions.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Conditional bulk updates
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('priority', 'IN', '1,2');
gr.setLimit(500);
gr.query();
var stats = { p1_updates: 0, p2_updates: 0 };
while (gr.next()) {
if (gr.priority == 1) {
// P1 gets immediate attention
if (gr.assigned_to.nil()) {
gr.assignment_group = 'Critical Response Team';
gr.work_notes = 'Auto-assigned to Critical Response Team';
gr.update();
stats.p1_updates++;
}
} else if (gr.priority == 2) {
// P2 gets SLA check
var age = GlideDateTime.subtract(new GlideDateTime(gr.sys_created_on), new GlideDateTime()).getDayPart();
if (age > 2 && gr.state == 1) {
gr.escalation = 1;
gr.work_notes = 'Auto-escalated: P2 older than 2 days without progress';
gr.update();
stats.p2_updates++;
}
}
}
gs.info('Conditional updates complete: ' + JSON.stringify(stats));
description: Apply conditional updates based on priority
Process large datasets in manageable batches.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Optimized batch processing
var CONFIG = {
table: 'incident',
query: 'active=true',
batchSize: 200,
maxBatches: 50,
pauseBetweenBatches: false // true adds 100ms delay
};
var totalProcessed = 0;
var batchNum = 0;
var startTime = new GlideDateTime();
while (batchNum < CONFIG.maxBatches) {
var gr = new GlideRecord(CONFIG.table);
gr.addEncodedQuery(CONFIG.query);
gr.orderBy('sys_created_on');
gr.chooseWindow(batchNum * CONFIG.batchSize, (batchNum + 1) * CONFIG.batchSize);
gr.query();
if (!gr.hasNext()) {
break;
}
var batchProcessed = 0;
while (gr.next()) {
// Process record (minimal operations for speed)
batchProcessed++;
}
totalProcessed += batchProcessed;
batchNum++;
// Progress logging every 10 batches
if (batchNum % 10 === 0) {
gs.info('Progress: Batch ' + batchNum + ', Total processed: ' + totalProcessed);
}
// Optional pause to reduce system load
if (CONFIG.pauseBetweenBatches) {
gs.sleep(100);
}
}
var endTime = new GlideDateTime();
var duration = GlideDateTime.subtract(startTime, endTime).getNumericValue() / 1000;
var rate = totalProcessed / duration;
gs.info('Complete: ' + totalProcessed + ' records in ' + duration.toFixed(2) + ' seconds (' + rate.toFixed(0) + ' records/sec)');
description: Optimized batch processing with metrics
Use efficient query patterns for bulk operations.
Use GlideAggregate for Counts:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Efficient counting with GlideAggregate
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.query();
var results = [];
while (ga.next()) {
results.push({
priority: ga.priority.getDisplayValue(),
count: parseInt(ga.getAggregate('COUNT'))
});
}
gs.info('Priority Distribution:\n' + JSON.stringify(results, null, 2));
description: Count incidents by priority efficiently
Use setWorkflow(false) for Bulk Updates:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Disable business rules for bulk operations
var gr = new GlideRecord('incident');
gr.addQuery('state', 7); // Closed
gr.addQuery('sys_updated_on', '<', gs.daysAgo(365));
gr.setLimit(1000);
gr.query();
// Disable workflows/business rules
gr.setWorkflow(false);
var count = 0;
while (gr.next()) {
gr.archived = true;
gr.update();
count++;
}
gs.info('Archived ' + count + ' old closed incidents (business rules disabled)');
description: Bulk archive with business rules disabled
Warning: Only disable workflows when you fully understand the implications.
Prevent memory issues with large datasets.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Memory-efficient processing
var BATCH_SIZE = 100;
var offset = 0;
var totalProcessed = 0;
var hasMore = true;
while (hasMore) {
// Use GlideRecord with window to avoid loading all records
var gr = new GlideRecord('cmdb_ci');
gr.addQuery('operational_status', 1);
gr.orderBy('sys_id'); // Consistent ordering
gr.chooseWindow(offset, offset + BATCH_SIZE);
gr.query();
if (gr.getRowCount() === 0) {
hasMore = false;
break;
}
while (gr.next()) {
// Process record - avoid storing in arrays
// Direct operations only
totalProcessed++;
}
offset += BATCH_SIZE;
// Check if we got fewer than batch size (last batch)
if (gr.getRowCount() < BATCH_SIZE) {
hasMore = false;
}
}
gs.info('Processed ' + totalProcessed + ' CI records');
description: Memory-efficient CI processing
Track successes and failures separately.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Comprehensive error tracking
var results = {
success: [],
errors: [],
skipped: []
};
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.setLimit(100);
gr.query();
while (gr.next()) {
try {
// Validation
if (gr.short_description.nil()) {
results.skipped.push({
number: gr.number.toString(),
reason: 'Missing short description'
});
continue;
}
// Attempt update
gr.work_notes = 'Batch processed on ' + new GlideDateTime().getDisplayValue();
gr.update();
results.success.push({
number: gr.number.toString(),
sys_id: gr.sys_id.toString()
});
} catch (e) {
results.errors.push({
number: gr.number.toString(),
error: e.message
});
}
}
gs.info('Results Summary:');
gs.info(' Success: ' + results.success.length);
gs.info(' Errors: ' + results.errors.length);
gs.info(' Skipped: ' + results.skipped.length);
if (results.errors.length > 0) {
gs.error('Errors:\n' + JSON.stringify(results.errors, null, 2));
}
if (results.skipped.length > 0) {
gs.warn('Skipped:\n' + JSON.stringify(results.skipped, null, 2));
}
description: Batch update with comprehensive error tracking
Implement retry logic for transient failures.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Retry pattern for batch operations
var MAX_RETRIES = 3;
var RETRY_DELAY = 1000; // milliseconds
function updateWithRetry(tableName, sysId, data) {
var attempts = 0;
var lastError = null;
while (attempts < MAX_RETRIES) {
attempts++;
try {
var gr = new GlideRecord(tableName);
if (gr.get(sysId)) {
for (var field in data) {
gr[field] = data[field];
}
gr.update();
return { success: true, attempts: attempts };
} else {
return { success: false, error: 'Record not found', attempts: attempts };
}
} catch (e) {
lastError = e.message;
gs.warn('Attempt ' + attempts + ' failed: ' + e.message);
if (attempts < MAX_RETRIES) {
gs.sleep(RETRY_DELAY * attempts); // Exponential backoff
}
}
}
return { success: false, error: lastError, attempts: attempts };
}
// Example usage
var recordsToUpdate = [
{ sys_id: '[sys_id_1]', data: { state: 2 } },
{ sys_id: '[sys_id_2]', data: { state: 2 } }
];
var results = [];
recordsToUpdate.forEach(function(record) {
var result = updateWithRetry('incident', record.sys_id, record.data);
result.sys_id = record.sys_id;
results.push(result);
});
gs.info('Update Results:\n' + JSON.stringify(results, null, 2));
description: Batch update with retry logic
Implement rollback for failed batch operations.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Batch operation with rollback capability
var operations = []; // Track for potential rollback
var allSuccess = true;
try {
// Operation 1
var inc1 = new GlideRecord('incident');
inc1.get('[incident_1_sys_id]');
var oldState1 = inc1.state.toString();
inc1.state = 6;
inc1.update();
operations.push({ table: 'incident', sys_id: inc1.sys_id.toString(), field: 'state', oldValue: oldState1 });
// Operation 2
var inc2 = new GlideRecord('incident');
inc2.get('[incident_2_sys_id]');
var oldState2 = inc2.state.toString();
inc2.state = 6;
inc2.update();
operations.push({ table: 'incident', sys_id: inc2.sys_id.toString(), field: 'state', oldValue: oldState2 });
// Operation 3 (simulated failure)
// throw new Error('Simulated failure for testing rollback');
gs.info('All ' + operations.length + ' operations completed successfully');
} catch (e) {
allSuccess = false;
gs.error('Error occurred: ' + e.message);
gs.info('Rolling back ' + operations.length + ' operations...');
// Rollback in reverse order
for (var i = operations.length - 1; i >= 0; i--) {
var op = operations[i];
try {
var rollback = new GlideRecord(op.table);
if (rollback.get(op.sys_id)) {
rollback[op.field] = op.oldValue;
rollback.setWorkflow(false); // Avoid triggering additional logic
rollback.update();
gs.info('Rolled back: ' + op.table + ' ' + op.sys_id);
}
} catch (rollbackError) {
gs.error('Rollback failed for ' + op.sys_id + ': ' + rollbackError.message);
}
}
gs.info('Rollback complete');
}
description: Batch operation with transaction-like rollback
Migrate data between tables with transformation.
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Table-to-table migration
var SOURCE_TABLE = 'x_old_app_requests';
var TARGET_TABLE = 'sc_request';
var BATCH_SIZE = 100;
var fieldMapping = {
'old_number': 'number',
'old_description': 'description',
'requestor': 'requested_for',
'submit_date': 'opened_at'
};
var migrated = 0;
var errors = 0;
var source = new GlideRecord(SOURCE_TABLE);
source.addQuery('migrated', false);
source.setLimit(BATCH_SIZE);
source.query();
while (source.next()) {
try {
var target = new GlideRecord(TARGET_TABLE);
target.initialize();
// Apply field mapping
for (var oldField in fieldMapping) {
var newField = fieldMapping[oldField];
target[newField] = source[oldField];
}
// Transform values
target.state = mapState(source.old_state);
target.priority = mapPriority(source.old_priority);
var newSysId = target.insert();
// Mark source as migrated
source.migrated = true;
source.migrated_to = newSysId;
source.update();
migrated++;
} catch (e) {
errors++;
gs.error('Error migrating ' + source.old_number + ': ' + e.message);
}
}
gs.info('Migration complete: ' + migrated + ' migrated, ' + errors + ' errors');
function mapState(oldState) {
var stateMap = { 'open': 1, 'in_progress': 2, 'closed': 3 };
return stateMap[oldState] || 1;
}
function mapPriority(oldPriority) {
var priorityMap = { 'critical': 1, 'high': 2, 'medium': 3, 'low': 4 };
return priorityMap[oldPriority] || 4;
}
description: Table-to-table data migration with transformation
Import data from external source (JSON array).
Tool: SN-Execute-Background-Script
Parameters:
script: |
// External data import pattern
var importData = [
{ email: '[email protected]', first_name: 'John', last_name: 'Doe', department: 'IT' },
{ email: '[email protected]', first_name: 'Jane', last_name: 'Smith', department: 'HR' },
{ email: '[email protected]', first_name: 'Bob', last_name: 'Wilson', department: 'Finance' }
];
var results = { created: 0, updated: 0, errors: 0 };
importData.forEach(function(data) {
try {
// Check for existing record
var existing = new GlideRecord('sys_user');
existing.addQuery('email', data.email);
existing.query();
if (existing.next()) {
// Update existing
existing.first_name = data.first_name;
existing.last_name = data.last_name;
existing.department = lookupDepartment(data.department);
existing.update();
results.updated++;
gs.info('Updated user: ' + data.email);
} else {
// Create new
var newUser = new GlideRecord('sys_user');
newUser.initialize();
newUser.email = data.email;
newUser.user_name = data.email.split('@')[0];
newUser.first_name = data.first_name;
newUser.last_name = data.last_name;
newUser.department = lookupDepartment(data.department);
newUser.active = true;
newUser.insert();
results.created++;
gs.info('Created user: ' + data.email);
}
} catch (e) {
results.errors++;
gs.error('Error importing ' + data.email + ': ' + e.message);
}
});
gs.info('Import complete: ' + JSON.stringify(results));
function lookupDepartment(name) {
var dept = new GlideRecord('cmn_department');
dept.addQuery('name', name);
dept.query();
if (dept.next()) {
return dept.sys_id.toString();
}
return '';
}
description: Import external user data with upsert logic
| Operation | MCP Tool | Purpose | |-----------|----------|---------| | Batch Create | SN-Batch-Create | Create multiple records | | Batch Update | SN-Batch-Update | Update multiple records by sys_id | | Parallel Create | SN-Create-Record (multiple) | Maximum throughput | | Parallel Update | SN-Update-Record (multiple) | Maximum throughput | | Complex Batch | SN-Execute-Background-Script | Advanced logic | | Query | SN-Query-Table | Find records for batch operations |
| Method | 100 Records | 1,000 Records | 10,000 Records | |--------|-------------|---------------|-----------------| | Serial MCP calls | ~50 sec | ~500 sec | Not practical | | Parallel MCP (10) | ~5 sec | ~50 sec | ~500 sec | | SN-Batch-Create | ~3 sec | ~30 sec | ~300 sec | | Background script | ~2 sec | ~20 sec | ~200 sec | | setWorkflow(false) | ~1 sec | ~10 sec | ~100 sec |
Symptom: Script execution stops mid-process Causes:
Symptom: "Out of memory" or slow performance Causes:
Symptom: Multiple copies of same record Causes:
Symptom: Expected side effects not occurring Causes:
admin/script-execution - Background script executionadmin/update-set-management - Track batch operation changesadmin/user-provisioning - Bulk user operationsadmin/deployment-workflow - Deploy batch scripts between instancestesting
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