What It Is
The deleteMultiple() method is GlideRecord's nuclear option for bulk deletion. It solves the performance disaster of looping through hundreds or thousands of records to delete them individually, executing a single SQL DELETE statement that wipes everything matching your query conditions. This isn't a convenience method—it's a necessity when dealing with large datasets where traditional delete loops would timeout, consume excessive memory, or take hours to complete. Without deleteMultiple(), bulk cleanup operations in ServiceNow would be practically impossible at enterprise scale.
Architecturally, deleteMultiple() operates exclusively on the server side, executing within the database layer through ServiceNow's transaction framework. There's no client-side equivalent because browser JavaScript cannot and should not have direct database access. This method bypasses the entire ServiceNow workflow engine—no Business Rules fire, no Script Actions execute, no notifications send, no audit records generate beyond basic delete tracking. It's a direct database operation wrapped in ServiceNow's security and transaction management, making it both powerful and dangerous.
Under the hood, ServiceNow translates your GlideRecord query into a prepared SQL DELETE statement, applies all active Access Control Rules, and executes it within a database transaction. The platform validates your session permissions, checks domain restrictions if domain separation is active, and ensures the deletion respects any active delete policies. However, it completely skips the record-level processing that makes ServiceNow's workflow engine so powerful—and sometimes so slow. This trade-off between performance and functionality is intentional and irreversible once the method executes.
You cannot efficiently clean up large datasets without deleteMultiple(). I've seen scheduled jobs that deleted old records one-by-one fail spectacularly when data volumes grew, timing out after processing only a fraction of the target records. Background scripts that worked fine in development with a few hundred test records became unusable in production with hundreds of thousands of real records. Maintenance operations that should run in seconds stretched to hours, blocking other transactions and degrading system performance. Without bulk deletion capability, ServiceNow instances become digital hoarders, accumulating data they cannot efficiently purge.
Platform administrators use deleteMultiple() for data lifecycle management—purging old log entries, removing test data, cleaning up orphaned records after major system changes. Developers reach for it during data migration scripts, emergency cleanup operations, and performance optimization projects. Architects design it into automated maintenance processes that keep instances healthy long-term. The method spans all skill levels because bulk deletion needs exist at every layer of ServiceNow operations, from routine housekeeping to crisis response.
The method sits in stark contrast to deleteRecord(), which processes one record at a time through the full ServiceNow stack, and setValue() combined with updateMultiple() for bulk modifications. Where deleteRecord() gives you complete workflow integration at the cost of performance, deleteMultiple() gives you raw speed at the cost of workflow integration. The choice between them defines whether you prioritize data integrity through business logic or operational efficiency through direct database access.
How It Works Under the Hood
When you call deleteMultiple(), ServiceNow doesn't iterate through your query results—it converts your GlideRecord query conditions directly into SQL WHERE clauses and executes a single DELETE statement against the database. The platform's query translator handles the conversion from ServiceNow's encoded query syntax to vendor-specific SQL, applying any necessary joins for reference fields or dot-walked conditions. This direct translation is why deleteMultiple() can delete thousands of records in milliseconds while a traditional loop would take minutes.
The security and transaction management happens at the database connection level, not the record level. ServiceNow validates your session's delete permissions for the target table before generating the SQL, applies domain restrictions as additional WHERE clauses, and wraps the entire operation in a database transaction that can be rolled back if something fails. However, because no individual records are loaded into memory, the platform cannot evaluate Business Rules, send notifications, or perform any of the record-level processing that developers often expect. The speed comes from bypassing the entire ServiceNow application layer and going straight to the database.
The Deletion Lifecycle
- ServiceNow validates your session has delete permissions for the target table and checks domain separation restrictions if enabled
- The platform translates your GlideRecord query conditions into SQL WHERE clauses, including any reference field joins or encoded query predicates
- Access Control Rules are applied as additional WHERE conditions to prevent deletion of records you shouldn't access
- A database transaction begins and the DELETE statement executes against the underlying table structure
- The database returns the count of deleted records, which ServiceNow returns as the method's result
- The transaction commits and basic audit trail entries are created, but no Business Rules fire and no notifications send
Business Rules, Script Actions, Notifications, and Workflows never execute during deleteMultiple operations. If your data integrity depends on these firing, you cannot use this method.
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Core Pattern
The fundamental pattern for deleteMultiple() involves careful query construction, permission validation, and count verification. Since the method bypasses all Business Rules, any cleanup logic that would normally execute during deletion must be handled explicitly before or after the bulk delete operation.
// Build query with specific conditions - be very careful here
var gr = new GlideRecord('sys_audit');
gr.addQuery('tablename', 'incident');
gr.addQuery('sys_created_on', '<', gs.daysAgoStart(90)); // Only records older than 90 days
gr.addQuery('field_name', 'NOT IN', 'state,priority'); // Exclude critical field changes
// ALWAYS count first to verify scope
var countQuery = new GlideRecord('sys_audit');
countQuery.addQuery('tablename', 'incident');
countQuery.addQuery('sys_created_on', '<', gs.daysAgoStart(90));
countQuery.addQuery('field_name', 'NOT IN', 'state,priority');
countQuery.query();
var expectedCount = countQuery.getRowCount();
gs.info('About to delete ' + expectedCount + ' audit records');
// Execute the bulk deletion
var deletedCount = gr.deleteMultiple();
// Verify results match expectations
if (deletedCount == expectedCount) {
gs.info('Successfully deleted ' + deletedCount + ' audit records');
} else {
gs.error('Delete count mismatch. Expected: ' + expectedCount + ', Actual: ' + deletedCount);
}var BulkDeleteUtils = Class.create();
BulkDeleteUtils.prototype = {
initialize: function() {
this.maxBatchSize = 10000; // Prevent timeouts on huge deletes
},
// Safely delete records in batches with validation
safeDeleteMultiple: function(tableName, encodedQuery, batchSize) {
batchSize = batchSize || this.maxBatchSize;
var totalDeleted = 0;
do {
var gr = new GlideRecord(tableName);
gr.addEncodedQuery(encodedQuery);
gr.setLimit(batchSize); // Process in manageable chunks
var deletedInBatch = gr.deleteMultiple();
totalDeleted += deletedInBatch;
gs.info('Deleted batch: ' + deletedInBatch + ' records from ' + tableName);
// Avoid infinite loops if no records deleted
if (deletedInBatch === 0) break;
} while (deletedInBatch >= batchSize);
return totalDeleted;
},
type: 'BulkDeleteUtils'
};Always verify your query returns the expected record count before calling deleteMultiple(). Use getRowCount() on an identical query to confirm scope.
Real-World Scenarios
Emergency Test Data Cleanup
After a load testing session created 50,000 fake incident records, the normal deletion process would take hours and potentially timeout. The test data needs immediate removal before business users notice the system slowdown.
// Target test incidents by specific naming pattern
var testIncidents = new GlideRecord('incident');
testIncidents.addQuery('short_description', 'STARTSWITH', 'LOAD_TEST_');
testIncidents.addQuery('caller_id', 'test.user@company.com'); // Additional safety check
testIncidents.addQuery('sys_created_on', '>', gs.hoursAgoStart(2)); // Only recent test records
// Verify scope before mass deletion
var verifyQuery = new GlideRecord('incident');
verifyQuery.addQuery('short_description', 'STARTSWITH', 'LOAD_TEST_');
verifyQuery.addQuery('caller_id', 'test.user@company.com');
verifyQuery.addQuery('sys_created_on', '>', gs.hoursAgoStart(2));
var targetCount = verifyQuery.getRowCount();
gs.info('Found ' + targetCount + ' test incidents to delete');
// Execute bulk deletion
var deleted = testIncidents.deleteMultiple();
gs.info('Deleted ' + deleted + ' test incidents in bulk operation');
// Report any discrepancy
if (deleted != targetCount) {
gs.warn('Count mismatch - expected: ' + targetCount + ', deleted: ' + deleted);
}Watch for cascade deletion effects if test records have child records—the database will handle referential integrity, but you won't get Business Rule notifications about the cascaded deletes. Always include multiple identifying conditions to prevent accidentally targeting real data that might match one criterion.
Automated Log Retention Policy
System logs accumulate rapidly and need regular cleanup to prevent database bloat. A scheduled job must delete logs older than 6 months while preserving error-level entries for compliance auditing.
// Define retention cutoff date
var cutoffDate = gs.daysAgoStart(180); // 6 months ago
// Clean up sys_log entries except errors and warnings
var logCleanup = new GlideRecord('syslog');
logCleanup.addQuery('sys_created_on', '<', cutoffDate);
logCleanup.addQuery('level', 'NOT IN', 'error,warn'); // Keep important entries
logCleanup.addQuery('source', '!=', 'Security'); // Preserve security logs
// Process in batches to avoid timeout
var batchSize = 5000;
logCleanup.setLimit(batchSize);
logCleanup.orderBy('sys_created_on'); // Delete oldest first
var totalDeleted = 0;
do {
var batchDeleted = logCleanup.deleteMultiple();
totalDeleted += batchDeleted;
gs.info('Log cleanup batch completed: ' + batchDeleted + ' records deleted');
// Reset query for next batch
logCleanup = new GlideRecord('syslog');
logCleanup.addQuery('sys_created_on', '<', cutoffDate);
logCleanup.addQuery('level', 'NOT IN', 'error,warn');
logCleanup.addQuery('source', '!=', 'Security');
logCleanup.setLimit(batchSize);
} while (batchDeleted >= batchSize);
gs.info('Log retention cleanup completed: ' + totalDeleted + ' total records deleted');Batch processing prevents transaction timeouts on large log tables, but requires careful query reconstruction for each iteration. Consider the impact on database performance during peak hours—schedule cleanup jobs during maintenance windows when possible.
Data Migration Orphan Cleanup
After migrating from a legacy ITSM system, thousands of orphaned attachment records remain that reference non-existent source records. These orphans consume storage and clutter queries, requiring bulk cleanup.
// Find attachments pointing to non-existent incident records
var orphanedAttachments = new GlideRecord('sys_attachment');
orphanedAttachments.addQuery('table_name', 'incident');
orphanedAttachments.addQuery('table_sys_id', '!=', ''); // Has a reference value
// Build list of valid incident sys_ids for comparison
var validIncidents = [];
var incidentCheck = new GlideRecord('incident');
incidentCheck.query();
while (incidentCheck.next()) {
validIncidents.push(incidentCheck.getUniqueValue());
}
gs.info('Found ' + validIncidents.length + ' valid incident records');
// Use NOT IN query to find orphans efficiently
orphanedAttachments.addQuery('table_sys_id', 'NOT IN', validIncidents.join(','));
// Count before deletion for verification
var orphanCount = orphanedAttachments.getRowCount();
gs.info('Found ' + orphanCount + ' orphaned attachments to delete');
// Execute bulk deletion of orphaned records
var deletedOrphans = orphanedAttachments.deleteMultiple();
gs.info('Deleted ' + deletedOrphans + ' orphaned attachment records');Large NOT IN queries can be expensive—consider breaking them into smaller chunks or using alternative approaches for massive datasets. Remember that deleting attachment records may not automatically clean up the associated file storage, depending on your instance configuration and retention policies.
The Classic Mistake
Using deleteMultiple() inside a loop or after modifying the query conditions.
// Business Rule attempting to delete related records
var incident = new GlideRecord('incident');
incident.addQuery('state', 7); // Closed
incident.addQuery('sys_created_on', '<', 'javascript:gs.daysAgoStart(365)');
incident.query();
while (incident.next()) {
// Delete related work notes first
var workNotes = new GlideRecord('sys_journal_field');
workNotes.addQuery('element_id', incident.sys_id);
workNotes.query();
workNotes.deleteMultiple(); // This executes immediately
// Now try to access the incident record
gs.info('Processing incident: ' + incident.number);
}This fails because deleteMultiple() executes immediately and closes the database cursor, but the outer while loop is still trying to iterate. You'll see "Invalid GlideRecord operation" errors in the System Log because ServiceNow can't maintain the iterator state after the deletion operation commits. The database transaction from deleteMultiple() interferes with the active query cursor from the outer loop. Browser-side, you'll see the script execution simply stop without completing the remaining iterations.
// Business Rule with proper separation of concerns
var incidentIds = [];
var incident = new GlideRecord('incident');
incident.addQuery('state', 7);
incident.addQuery('sys_created_on', '<', 'javascript:gs.daysAgoStart(365)');
incident.query();
// First pass: collect IDs
while (incident.next()) {
incidentIds.push(incident.sys_id.toString());
gs.info('Found incident for cleanup: ' + incident.number);
}
// Second pass: bulk delete operations outside the loop
var workNotes = new GlideRecord('sys_journal_field');
workNotes.addQuery('element_id', 'IN', incidentIds.join(','));
workNotes.query();
workNotes.deleteMultiple();Never call deleteMultiple() while iterating through a GlideRecord result set. Always collect your target records first, then execute bulk operations afterward.
Performance Rules
- Limit
deleteMultiple()operations to under 10,000 records per execution. Beyond this threshold, you'll hit the 30-second script timeout and get incomplete deletions with no error message to the user. - Use
chooseWindow()beforedeleteMultiple()for large datasets. Without chunking, deleting over 5,000 records can cause database lock contention and make other users experience slow page loads. - Never call
deleteMultiple()on tables with complex reference relationships without checkingsys_db_objectdependencies first. Deleting parent records can trigger cascading reference updates that multiply your operation time by 10x. - Avoid
deleteMultiple()during peak business hours (typically 9 AM - 5 PM in your instance timezone). Large deletion operations can saturate database I/O and trigger admin alerts about system performance degradation. - Check
getRowCount()before executing deletion on user-input driven queries. Without validation, users can accidentally trigger deletion of your entire table, requiring database restoration from backup. - Wrap
deleteMultiple()in try-catch blocks when used in Scheduled Jobs or Flow Actions. Database deadlocks during bulk operations cause silent failures that won't appear in the execution history. - Use
setWorkflow(false)beforedeleteMultiple()when deleting over 1,000 records. Workflow processing on each deleted record can increase execution time from seconds to minutes, causing user session timeouts.
Side Effects & Platform Behavior
- Business Rules are completely bypassed - no Before Delete, After Delete, or Async rules fire. The
sys_audit_deletetable still captures the deletion events, but custom cleanup logic in Business Rules won't execute. - Access Controls (ACLs) are enforced per record - if the user lacks delete rights to any record in the query, the entire operation fails with a security exception logged to
syslog. - Notifications and Email Scripts won't trigger because they depend on Business Rules. Users expecting deletion confirmations or audit emails will receive nothing.
- Workflow activities and transitions are skipped entirely. Any workflow context or approval processes attached to the deleted records terminate without completion logging.
- Journal fields and activity entries are automatically deleted from
sys_journal_fieldandsys_activitytables through foreign key cascade rules, not application logic. - The
gs.getSession().getClientData()cache is not cleared, so client-side code may still reference deleted record data until page refresh. - Attachment records in
sys_attachmentare deleted but the actual file cleanup in the filesystem happens asynchronously, potentially hours later. - Database triggers still fire normally, so any custom SQL-level constraints or logging you've configured at the database layer will execute.
- Flow Designer flows that trigger on record deletion won't activate because they depend on Business Rule execution. ServiceNow Integration Hub spokes expecting deletion events won't receive them.
- Reports and dashboards showing deleted record counts update immediately, but any custom metrics calculated in Business Rules won't reflect the deletions until manual recalculation.
Debugging When It Breaks
The most common failure symptom is silent partial completion - some records delete successfully while others remain, with no error message displayed to users. You'll typically see this when ACL violations occur mid-operation or when database constraints prevent certain deletions. The operation appears successful in the UI, but a manual count reveals fewer deletions than expected.
For comprehensive debugging, check System Log > All and filter by your session ID to see security violations and constraint errors. The Script Debugger won't help much since deleteMultiple() executes at the database level. Look for log entries containing "ACL Security constraint" or "Foreign key constraint" - these indicate why specific records weren't deleted. Browser console errors are rare unless you're calling this from a client script, which you shouldn't be doing anyway.
Quick diagnostic checklist when deletions fail:
- Verify your query actually returns records with
getRowCount()before callingdeleteMultiple() - Check if you have delete ACL permissions by testing
canDelete()on a single record first - Review
sys_db_objectfor tables with "Delete rule" set to "Restrict" that might block the operation - Look for active transactions or locks by checking if other users are modifying the same records simultaneously
- Test with a smaller record set using
chooseWindow(0, 10)to isolate timeout vs. permission issues
Quick Reference
- Returns void - you can't chain methods or check success status afterward like you can with
deleteRecord() - Completely ignores
setWorkflow(false)setting - workflows are always bypassed regardless - Works in scoped applications but respects cross-scope access controls - can't delete records from other application scopes without explicit access
- Automatically commits the transaction - you can't rollback a
deleteMultiple()operation once executed - Fails silently on reference fields with "Restrict delete" rules - check
sys_dictionaryfor "Delete" column values - Respects field-level ACLs in addition to table ACLs - a single restricted field can block the entire operation
- Creates audit records in
sys_audit_deletewith limited field information compared to Business Rule deletions - Cannot be used inside client scripts - throws "Illegal operation" error if attempted from UI Actions or Client Scripts
- Updates table statistics immediately but cached list views may show stale counts until next page refresh or manual reload
- Triggers database replication immediately in clustered environments - useful for time-sensitive cleanup operations