Find your people. Pick a challenge. Ship something real. The CreatorCon Hackathon is coming to the Community Pavilion for one epic night. Every skill level, every role welcome. Join us on May 5th and learn more here.

Can we prevent records getting deleted by Auto Flush (sys_auto_flush) without modifying the rule.

Not applicable

Hello,

I have a requirement where we need to prevent the records which are getting deleted by the auto flush rule, I don't want to modify the auto flush rule. I just want to prevent the records which are getting deleted though auto flush rule or any of the archive rule.

This requirement came after we identified an issue in our system where, their were multiple auto flush rules were running in the production on multiple tables because of which our records got deleted. So, we inactivated those rule after that. But now we are looking for a solution where we can prevent these records from getting delete in future if someone creates any such auto flush rule.

Also, if a record is getting deleted we want to send out a notification for those records which are getting deleted. Now in this scenario there are multiple tables, can we create a single BR where we can store the values of the tables in a system property and that can trigger the notification.

1 ACCEPTED SOLUTION

Naveen20
ServiceNow Employee

Try this complete approach


1. System Property for Protected Tables

First, create a system property to store the list of tables you want to protect. Navigate to sys_properties.list and create:

  • Name: x_custom.protected_tables
  • Value: incident,change_request,sc_req_item (comma-separated list of tables)

This gives you a central place to manage which tables are protected without modifying any business rules.


2. Shared Script Include for Reusable Logic

Create a Script Include that holds your protection and notification logic so every table's Business Rule can call into it:

var AutoFlushProtector = Class.create();
AutoFlushProtector.prototype = {
    initialize: function() {
        this.protectedTables = gs.getProperty('x_custom.protected_tables', '').split(',');
    },

    isProtectedTable: function(tableName) {
        for (var i = 0; i < this.protectedTables.length; i++) {
            if (this.protectedTables[i].trim() === tableName) {
                return true;
            }
        }
        return false;
    },

    isAutoFlushContext: function() {
        // Auto flush runs as 'system' with no interactive session
        var userName = gs.getUserName();
        var sessionUser = gs.getSession().isInteractive();
        return (userName === 'system' && !sessionUser);
    },

    handleDelete: function(current) {
        var tableName = current.getTableName();

        if (!this.isProtectedTable(tableName)) {
            return; // not a protected table, allow delete
        }

        if (this.isAutoFlushContext()) {
            // Fire event for notification before aborting
            gs.eventQueue(
                'x_custom.auto_flush_blocked',
                current,
                tableName,
                current.getDisplayValue()
            );

            gs.addErrorMessage('Delete blocked: Auto flush attempted to remove ' 
                + tableName + ' record: ' + current.getDisplayValue());
            current.setAbortAction(true);

            // Optional: log to a custom tracking table
            this._logBlockedDeletion(current, tableName);
        }
    },

    _logBlockedDeletion: function(current, tableName) {
        var log = new GlideRecord('x_custom_flush_block_log'); // optional custom table
        log.initialize();
        log.setValue('table_name', tableName);
        log.setValue('record_sys_id', current.getUniqueValue());
        log.setValue('display_value', current.getDisplayValue());
        log.setValue('blocked_on', new GlideDateTime());
        log.insert();
    },

    type: 'AutoFlushProtector'
};

3. Before Delete Business Rule (Per Table)

Unfortunately, a single Business Rule cannot run on multiple tables natively in ServiceNow. You need one BR per table. However, since all the logic lives in the Script Include, each BR is just a two-line wrapper.

Create a Before Delete Business Rule on each protected table:

  • Name: Protect from Auto Flush - [Table Label]
  • Table: e.g., incident
  • When: Before
  • Delete: true
  • Order: 50 (run early)

Script:

(function executeRule(current, previous) {
    var protector = new AutoFlushProtector();
    protector.handleDelete(current);
})(current, previous);

You repeat this BR on each table listed in the property. Since the script body is identical, it takes seconds to clone for additional tables.


4. Notification via Event

Register the event and tie a notification to it:

Event Registration sysevent_register.list

  • Name: x_custom.auto_flush_blocked
  • Table: Global or *

Notification sysevent_email_action.list

  • Name: Auto Flush Deletion Blocked
  • Event Name: x_custom.auto_flush_blocked
  • Recipients: Your admin group or DL
  • Subject: Auto Flush Blocked: ${event.parm1} - ${event.parm2}
  • Body: Include the table name (event.parm1) and display value (event.parm2) so the team knows exactly which record was targeted.

 

View solution in original post

4 REPLIES 4

Naveen20
ServiceNow Employee

Try this complete approach


1. System Property for Protected Tables

First, create a system property to store the list of tables you want to protect. Navigate to sys_properties.list and create:

  • Name: x_custom.protected_tables
  • Value: incident,change_request,sc_req_item (comma-separated list of tables)

This gives you a central place to manage which tables are protected without modifying any business rules.


2. Shared Script Include for Reusable Logic

Create a Script Include that holds your protection and notification logic so every table's Business Rule can call into it:

var AutoFlushProtector = Class.create();
AutoFlushProtector.prototype = {
    initialize: function() {
        this.protectedTables = gs.getProperty('x_custom.protected_tables', '').split(',');
    },

    isProtectedTable: function(tableName) {
        for (var i = 0; i < this.protectedTables.length; i++) {
            if (this.protectedTables[i].trim() === tableName) {
                return true;
            }
        }
        return false;
    },

    isAutoFlushContext: function() {
        // Auto flush runs as 'system' with no interactive session
        var userName = gs.getUserName();
        var sessionUser = gs.getSession().isInteractive();
        return (userName === 'system' && !sessionUser);
    },

    handleDelete: function(current) {
        var tableName = current.getTableName();

        if (!this.isProtectedTable(tableName)) {
            return; // not a protected table, allow delete
        }

        if (this.isAutoFlushContext()) {
            // Fire event for notification before aborting
            gs.eventQueue(
                'x_custom.auto_flush_blocked',
                current,
                tableName,
                current.getDisplayValue()
            );

            gs.addErrorMessage('Delete blocked: Auto flush attempted to remove ' 
                + tableName + ' record: ' + current.getDisplayValue());
            current.setAbortAction(true);

            // Optional: log to a custom tracking table
            this._logBlockedDeletion(current, tableName);
        }
    },

    _logBlockedDeletion: function(current, tableName) {
        var log = new GlideRecord('x_custom_flush_block_log'); // optional custom table
        log.initialize();
        log.setValue('table_name', tableName);
        log.setValue('record_sys_id', current.getUniqueValue());
        log.setValue('display_value', current.getDisplayValue());
        log.setValue('blocked_on', new GlideDateTime());
        log.insert();
    },

    type: 'AutoFlushProtector'
};

3. Before Delete Business Rule (Per Table)

Unfortunately, a single Business Rule cannot run on multiple tables natively in ServiceNow. You need one BR per table. However, since all the logic lives in the Script Include, each BR is just a two-line wrapper.

Create a Before Delete Business Rule on each protected table:

  • Name: Protect from Auto Flush - [Table Label]
  • Table: e.g., incident
  • When: Before
  • Delete: true
  • Order: 50 (run early)

Script:

(function executeRule(current, previous) {
    var protector = new AutoFlushProtector();
    protector.handleDelete(current);
})(current, previous);

You repeat this BR on each table listed in the property. Since the script body is identical, it takes seconds to clone for additional tables.


4. Notification via Event

Register the event and tie a notification to it:

Event Registration sysevent_register.list

  • Name: x_custom.auto_flush_blocked
  • Table: Global or *

Notification sysevent_email_action.list

  • Name: Auto Flush Deletion Blocked
  • Event Name: x_custom.auto_flush_blocked
  • Recipients: Your admin group or DL
  • Subject: Auto Flush Blocked: ${event.parm1} - ${event.parm2}
  • Body: Include the table name (event.parm1) and display value (event.parm2) so the team knows exactly which record was targeted.

 

Not applicable

Thankyou for such elaborated solution. It worked as per the requirement.

Not applicable

Hi @Naveen20 , thank you for the providing the solution. While this approach is working fine in my personal instance, but I am facing following issues in the customer instance. :

1. While the script is initially preventing the records from getting deleted, but after sometime the records are still getting deleted somehow through the auto flush rule only. I am not sure why, though I know there are following scheduled script which triggers the auto flush rule, is there is some additional script which runs for auto flush rule apart from this, then please let me know :
a. DMScheduler-12Hourly
b. DMScheduler-Hourly
c. DMScheduler-Daily

2. I tried adding "gs.addErrorMessage" and "gs.error" in the script to log the error and see which record got prevented from getting deleted still the system is not able to log any errors in the log table.

The two issues you're facing on the customer instance are related, and here's what's happening:

Issue 1: Records still getting deleted despite the prevention script

The three scheduled jobs you identified (DMScheduler-12Hourly, DMScheduler-Hourly, DMScheduler-Daily) are indeed the core schedulers that trigger Data Management operations, including auto flush. However, the critical thing to understand is how the auto flush engine actually deletes records — it does not use a standard GlideRecord.deleteRecord() call that your Business Rule can intercept.

The Data Management framework uses internal Java-level APIs (the com.snc.data_management package) to perform the flush operations. These deletions happen at a lower level than the Business Rule engine, which means:

  • A before delete Business Rule on your table may not fire at all when the auto flush rule processes records, because the DM engine can bypass the standard GlideRecord delete path depending on the table type and configuration.
  • On OOTB tables, the archiving/flush framework has specific integration points. On custom tables, the behavior can differ — the engine may use deleteMultiple() or direct SQL-level operations that skip Business Rule execution entirely.

This is almost certainly why your script works initially (perhaps catching manual deletes or other triggers) but the scheduled auto flush still removes records.

How to actually prevent the auto flush from deleting specific records:

  1. Modify the Auto Flush Rule itself — Go to sys_auto_flush.list and open the flush rule targeting your table. Add a filter condition that excludes the records you want to preserve. This is the most reliable approach because you're preventing the flush engine from even selecting those records, rather than trying to intercept the delete downstream.

  2. Deactivate the specific flush rule — If you don't want any records flushed from this table, simply deactivate the flush rule. You can create a new one with a more restrictive condition if needed.

  3. Use an Archive Rule instead — If the goal is to retain records long-term but move them out of the live table, configure an Archive Rule (sys_archive_rule) instead of relying on flush, which is destructive by design.

Issue 2: gs.addErrorMessage and gs.error not logging anything

This confirms what I described above — if the Business Rule never fires (because the DM engine bypasses it), then your logging statements will never execute either. That's why you see no entries in the system log. The absence of log entries is actually the diagnostic proof that the auto flush framework is not going through the Business Rule layer for its delete operations.

If you want to confirm this, try a manual delete of a record from the list view — your Business Rule should fire and you should see the log entries. The fact that it logs on manual delete but not on auto flush confirms the engine bypass.

In summary: Don't try to intercept auto flush deletes with a Business Rule — modify or deactivate the flush rule condition directly at sys_auto_flush.list. That's the only reliable way to prevent the Data Management framework from removing records on customer instances.