Skip to Content

Cleanup Old Audit Records

Automatically delete audit and history records older than specified retention period to maintain database performance.

#scheduled-job #cleanup #audit #maintenance #performance #retention

Script Code

JavaScript
1// Audit Record Cleanup Scheduled Job
2// Runs daily to remove old audit records based on retention policy
3
4try {
5    // Configuration: Define retention periods in days
6    var retentionPolicies = {
7        'sys_audit': 90,           // System audit records - 90 days
8        'sys_journal_field': 180, // Journal/work notes - 180 days  
9        'sys_history_line': 365,  // History records - 1 year
10        'sys_audit_delete': 30,   // Deletion audit - 30 days
11        'u_custom_audit': 60      // Custom audit table - 60 days
12    };
13
14    // Configuration: Batch size for deletion (to avoid performance issues)
15    var batchSize = 1000;
16
17    // Configuration: Enable/disable cleanup per table
18    var enabledTables = {
19        'sys_audit': true,
20        'sys_journal_field': true,
21        'sys_history_line': false,  // Disabled - keep all history
22        'sys_audit_delete': true,
23        'u_custom_audit': true
24    };
25
26    var totalDeleted = 0;
27    var processedTables = [];
28
29    // Process each table in retention policy
30    for (var tableName in retentionPolicies) {
31        if (!enabledTables[tableName]) {
32            gs.info('Audit Cleanup: Skipping disabled table: ' + tableName);
33            continue;
34        }
35
36        var retentionDays = retentionPolicies[tableName];
37        var deletedCount = cleanupTable(tableName, retentionDays, batchSize);
38        
39        if (deletedCount > 0) {
40            totalDeleted += deletedCount;
41            processedTables.push(tableName + ': ' + deletedCount + ' records');
42            gs.info('Audit Cleanup: Deleted ' + deletedCount + ' records from ' + tableName);
43        }
44    }
45
46    // Log summary
47    if (totalDeleted > 0) {
48        gs.info('Audit Cleanup Job completed. Total deleted: ' + totalDeleted + ' records across ' + processedTables.length + ' tables');
49        gs.eventQueue('audit.cleanup.completed', null, totalDeleted, processedTables.join(', '));
50    } else {
51        gs.info('Audit Cleanup Job completed. No records deleted.');
52    }
53
54    /**
55     * Clean up records from specified table
56     * @param {String} tableName - Name of table to clean
57     * @param {Number} retentionDays - Days to retain
58     * @param {Number} batchSize - Records to delete per batch
59     * @returns {Number} Number of records deleted
60     */
61    function cleanupTable(tableName, retentionDays, batchSize) {
62        try {
63            // Calculate cutoff date
64            var cutoffDate = new GlideDateTime();
65            cutoffDate.addDaysLocalTime(-retentionDays);
66
67            gs.info('Cleaning table: ' + tableName + ', cutoff date: ' + cutoffDate.getDisplayValue());
68
69            var totalDeleted = 0;
70            var hasMoreRecords = true;
71
72            // Process in batches to avoid timeout
73            while (hasMoreRecords) {
74                var gr = new GlideRecord(tableName);
75                
76                // Add date filter based on table structure
77                if (tableName === 'sys_audit' || tableName === 'sys_audit_delete') {
78                    gr.addQuery('sys_created_on', '<', cutoffDate);
79                } else if (tableName === 'sys_journal_field') {
80                    gr.addQuery('sys_created_on', '<', cutoffDate);
81                    // Only delete work notes/comments, not all journal entries
82                    gr.addQuery('element', 'IN', 'work_notes,comments,close_notes');
83                } else if (tableName === 'sys_history_line') {
84                    gr.addQuery('sys_created_on', '<', cutoffDate);
85                } else {
86                    // Custom table - assume standard sys_created_on field
87                    gr.addQuery('sys_created_on', '<', cutoffDate);
88                }
89
90                // Add additional filters for specific tables
91                if (tableName === 'sys_audit') {
92                    // Skip certain critical audit records
93                    gr.addQuery('tablename', 'NOT IN', 'sys_user,sys_user_role,sys_security_acl');
94                }
95
96                gr.setLimit(batchSize);
97                gr.query();
98
99                var batchCount = 0;
100                while (gr.next()) {
101                    gr.deleteRecord();
102                    batchCount++;
103                }
104
105                totalDeleted += batchCount;
106
107                // Check if we have more records to process
108                if (batchCount < batchSize) {
109                    hasMoreRecords = false;
110                }
111
112                // Add small delay between batches
113                if (hasMoreRecords) {
114                    gs.sleep(1000); // 1 second delay
115                }
116            }
117
118            return totalDeleted;
119
120        } catch (e) {
121            gs.error('Error cleaning table ' + tableName + ': ' + e.message);
122            return 0;
123        }
124    }
125
126    /**
127     * Archive records instead of deleting (alternative approach)
128     * @param {String} tableName - Source table
129     * @param {String} archiveTable - Archive table name
130     * @param {Number} retentionDays - Days to retain in source
131     */
132    function archiveRecords(tableName, archiveTable, retentionDays) {
133        try {
134            var cutoffDate = new GlideDateTime();
135            cutoffDate.addDaysLocalTime(-retentionDays);
136
137            var grSource = new GlideRecord(tableName);
138            grSource.addQuery('sys_created_on', '<', cutoffDate);
139            grSource.setLimit(batchSize);
140            grSource.query();
141
142            var archivedCount = 0;
143
144            while (grSource.next()) {
145                // Create archive record
146                var grArchive = new GlideRecord(archiveTable);
147                grArchive.initialize();
148
149                // Copy all fields
150                var fields = grSource.getFields();
151                for (var i = 0; i < fields.size(); i++) {
152                    var field = fields.get(i);
153                    var fieldName = field.getName();
154                    if (fieldName !== 'sys_id') {
155                        grArchive.setValue(fieldName, grSource.getValue(fieldName));
156                    }
157                }
158
159                grArchive.u_original_sys_id = grSource.sys_id.toString();
160                grArchive.u_archived_date = new GlideDateTime();
161
162                var archiveId = grArchive.insert();
163                if (archiveId) {
164                    grSource.deleteRecord();
165                    archivedCount++;
166                }
167            }
168
169            return archivedCount;
170
171        } catch (e) {
172            gs.error('Error archiving records from ' + tableName + ': ' + e.message);
173            return 0;
174        }
175    }
176
177} catch (e) {
178    gs.error('Audit Cleanup Job error: ' + e.message);
179    // Send alert for job failure
180    gs.eventQueue('audit.cleanup.failed', null, e.message, '');
181}

How to Use

1. Navigate to System Definition > Scheduled Jobs\n2. Click 'New' to create a new scheduled job\n3. Set Name to 'Audit Cleanup Job'\n4. Set Run to 'Daily' at desired time (recommend off-peak hours)\n5. Paste the script code\n6. Configure retention periods in the script\n7. Test in a development environment first\n8. Monitor execution logs and adjust batch sizes if needed\n9. Create archive tables if using archive approach\n10. Set up event notifications for job completion/failure

Explore More Scripts

Browse our complete library of ServiceNow scripts

View All Scripts