The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Maik Skoddow
Tera Patron
Tera Patron
tool-gcb78f6529_1280.jpg

If you want to get notified when new Business Rules are added by me, you can subscribe to this page!

 

Over time, every developer or system administrator builds little helpers that make working with ServiceNow more convenient and secure. This is also true for me, and in this article I present a few Business Rules that have been collecting in my toolbox.

 

 

What are Business Rules?

 

Business Rules are server-side logic that execute when database records are queried, updated, inserted, or deleted. Business Rules respond to database interactions regardless of access method: for example, users interacting with records through forms or lists, web services, or data imports (configurable). They do not monitor forms or form fields but do execute their logic when forms interact with the database such as when a record is saved, updated, or submitted.

 

Please refer to the ServiceNow documentation pages if you want to learn more about Business Rules.

        
Table of Contents
  1. Prevent direct assignment of licensable roles
  2. Prevent adding artifacts to an update set which is not "In Progress" state
  3. Force creation of personal artifacts in "Global"
  4. Prevent sending of unwanted emails
  5. Set agent's availability automatically
  6. Reduce syslog list results to the last 15 minutes
  7. Display all suites an instance scan check is contained in
  8. Display a warning in case an OOTB artifact has been modified
  9. Block large profile pictures
  10. Display a warning if an artifact was skipped
  11. Add relationships "Updates" & "Versions" automatically to the form
  12. Warn if translations are missing for the name of the UI Action
  13. Enforce capturing of scheduled scripts for committing to Git

 

 

 

Prevent direct assignments of licensable roles to users

 

Except for the admin user, the following Business Rule prevents a licensable role from being assigned directly to a user. If you are interested: I have written a separate article on the underlying reasons.

 

 

Business Rule

Table

sys_user_has_role

Advanced

true

When

before

Insert

true

Filter Conditions

MaikSkoddow_1-1671370841592.png

Script

(function executeRule(current, previous /*null when async*/) {
	function _getContainedRoles(strRole, arrRoles) {
		var _arrRoles = arrRoles || [strRole];
		var _grRoles  = new GlideRecord('sys_user_role_contains');

		_grRoles.addQuery('role.name', strRole);
		_grRoles.query();

		while(_grRoles.next()){
			if (!_grRoles.contains.nil()) {
				var _strContainedRole = _grRoles.contains.name.toString();
				
				_arrRoles.push(_strContainedRole);
				_getContainedRoles(_strContainedRole, _arrRoles);
			}
		}

		return new ArrayUtil().unique(_arrRoles);
	}

	var _strRoleName   = current.role.getDisplayValue();
	var _strUserName   = current.user.getDisplayValue();
	var _grLicenseRole = new GlideRecord('license_role');
	var _arrAllRoles   = _getContainedRoles(_strRoleName);

	_grLicenseRole.addEncodedQuery(
		'license_role_typeISNOTEMPTY^license_role_type.name!=requester^nameIN' +
		_arrAllRoles.join(',')
	);
	_grLicenseRole.setLimit(1);
	_grLicenseRole.query();

	if (_grLicenseRole.hasNext()) {
		gs.addErrorMessage(
			gs.getMessage(
				'Role "{0}" or one of its contained roles require a license and ' +
				'therefore cannot be assigned to user "{1}" directly. ' +
				'Instead use groups for role assignments.',
				[_strRoleName, _strUserName]
			)
		);
		current.setAbortAction(true);
	}

})(current, previous);

 

 

 

Prevent adding artifacts to an update set which is not "In Progress" state

 

I experienced inexplicable affects on a ServiceNow instance where a release had recently been deployed the other day within one of my projects. After a long investigation, it was identified that one of the deployed Update Sets contained an artifact that had been inserted after the Update Set was already closed. Yes, you read that correctly. ServiceNow does not restrict the addition of artifacts to an already closed Update Set.

MaikSkoddow_0-1675517596875.png

However, you can avoid this with a simple Business Rule on the sys_update_xml table and spare yourself a lot of hassles.

 

 

Business Rule

Table

sys_update_xml

Advanced

true

When

before

Update

true

Filter Conditions

MaikSkoddow_1-1675518109336.png

Script

(function executeRule(current, previous) {

	current.setAbortAction(true);

	gs.addErrorMessage(
		gs.getMessage(
			'Cannot move "{0}" to Update Set "{1}" which is in "{2}" State!', [
				current.getValue('name'), 
				current.getDisplayValue('update_set'), 
				current.update_set.state.getDisplayValue()
			]
		)
	);

})(current, previous);

 

 

 

Force creation of personal artifacts in the "Global" scope

 

Have you ever noticed these strange artifacts (aka application files) in your app? 

MaikSkoddow_0-1675568326757.png

In ServiceNow, there are a number of artifact types that, although personal in nature, have an "Application" column and are thus captured in applications.


This happens, for example, whenever a user personalizes a list and happens to select an application under development in the Application Picker.

 

This is where the following business rule comes into play: If one of the three artifact types I have identified so far is to be created and the target application is not "Global", the creation in the Global Scope is enforced accordingly.

 

 

Business Rule

Table

sys_metadata

Advanced

true

When

before

Order

10000

Insert

true

Filter
Conditions

MaikSkoddow_1-1675568827379.png

Actions

MaikSkoddow_2-1675568897716.png

 

 

 

Prevent sending of unwanted emails

 

A fresh ServiceNow instance already has tons of activated email notifications, and each new plugin you are going to install might be shipped with additional ones.

Most system administrators would iterate the list and deactivate all notifications which are not required. But this approach has several downsides:

  • This blacklist approach is rather time-consuming and with every new installed plugin you have to check again all new notifications.
  • There is a risk of creating so-called "skipped records" for all of these deactivated notifications in the next system upgrade.
  • All deactivations have to be captured in an update set to provision these changes to all other instances.

 

Instead, use a whitelist approach to let the system send only emails from allowed notifications. Such a whitelist could for example be maintained via a system property. And then you need the following Business Rule which identifies the underlying notification for an email to be sent. If the notification doesn't pass the whitelist, the respective email record is set to "send-ignored".

 

 

Business Rule

 

⚠️ Please note:
The script part in the following Business Rule has to be modified first according to your needs before you can use it. My below example will ignore all emails with notification records in the "global" scope.

 

 

Table

sys_email_log

Advanced

true

When

after

Insert

true

Script

(function executeRule(current, previous) {

	var _grEmailLog = new GlideRecord('sys_email_log');

	_grEmailLog.addQuery('notificationISNOTEMPTY^email=' + current.getValue('sys_id'));
	_grEmailLog.setLimit(1);
	_grEmailLog.query();

	if (_grEmailLog.next()) {
		var _strScope = String(_grEmailLog.notification.sys_scope.scope || '');

		if (_strScope === 'global') {	   
			current.setValue('type', 'send-ignored');
			current.update();
		}
	}

})(current, previous);

 

 

 

Set agent's availability automatically

 

If you have installed the plugin Advanced Work Assignment in your instance, agents can set their presence status to "Available" in the inbox module of any workspace to indicate they are ready for receiving tickets:

MaikSkoddow_1-1676629400645.png

And as most of the agents are just working in ServiceNow by dealing with any assigned work, they are asking to set the presence status automatically.

 

The following Business Rule will do this by catching the login and writing the status to table awa_agent_presence accordingly.

 

 

Business Rule

 

Table

sys_user

Advanced

true

When

async

Update

true

Filter
Conditions

MaikSkoddow_0-1676629151377.png

Condition

gs.tableExists('awa_agent_presence') && gs.hasRole('awa_agent')

Script

(function executeRule(current, previous) {

	var grPresence = new GlideRecord("awa_agent_presence");

	if (!grPresence.get("agent", current.getUniqueValue())) {
		grPresence.initialize();		
	}

	grPresence.setDisplayValue("current_presence_state", "Available");
	grPresence.update();

})(current, previous);

 

 

 

Reduce syslog list results to the last 15 minutes

 

Depending on which instance you are, what the configured log level is and how many log outputs your applications are creating, the number of records in the syslog table can be many millions large. Opening such a large table via "syslog.list" in the application navigator will block your session for some time or even cancel the transaction.

 

Therefore, it is better to enter "syslog.filter", which prevents the initial loading of the data and allows you to configure a filter accordingly instead. For example, you can specify that only records created in the last 15 minutes should be returned.

 

But how many times have you been unaware of this? For this reason, the following "before query" Business Rule checks whether a filter expression has been configured for the sys_created_on field. If not, a filter expression is added that restricts the results to those created in the last 15 minutes. You can override this restriction by explicitly configuring a filter expression for the sys_created_on field.

MaikSkoddow_0-1684027413815.png

 

Business Rule

 

Table

syslog

Advanced

true

When

before

Query

true

Condition

gs.getSession().isInteractive()

Script

(function executeRule(current) {

	var _strCurrentTable = (current.getTableName() || '').toString();
	var _strCurrentURL   = (gs.action.getGlideURI() || '').toString();
	
	//only continue with list views in the Classic UI
	if (!_strCurrentURL.startsWith(_strCurrentTable + '_list.do')) {
		return;
	}
	
	var strEncodedQuery  = current.getEncodedQuery() || '';
	var numStartPosition = 
		strEncodedQuery.endsWith('ORDERBYDESCsys_created_on') || 
		strEncodedQuery.endsWith('ORDERBYsys_created_on')  ? 
			strEncodedQuery.length - 15 : 
			strEncodedQuery.length;
	
	if (strEncodedQuery.lastIndexOf('sys_created_on', numStartPosition) === -1) {
		current.addEncodedQuery(
			'sys_created_onONLast 15 minutes@javascript:gs.beginningOfLast15Minutes()' +
			'@javascript:gs.endOfLast15Minutes()'
		);
			
		gs.addInfoMessage(
			'For performance reasons your query was extended by an expression to narrow ' +
			'down the result list to entries which have been created in the last 15 min. ' +
			'You can override this setting by using a dedicated query expression for the ' +
			'"Created on" field. But be careful: If the query result set is very large, ' +
			'it will cause a performance decrease!'
		);
	}

})(current);

 

 

 

Display all suites an instance scan check is contained in

 

Scan Checks in ServiceNow are related to any Scan Suites via the many-to-many table. For this reason, you cannot configure a corresponding related list on a Scan Check form. OOTB ServiceNow provides a Relationships "Check" you could configure instead:

MaikSkoddow_0-1678018452749.png

But honestly that's not the best user experience as you have to scroll down each time you open a Scan Check. However, I need this information right at the beginning of the form, so I wrote the following "display" business rule, which displays the list of all referencing Scan Suites:

MaikSkoddow_1-1678018671544.png

 

Business Rule

 

Table

scan_check

Advanced

true

When

display

Condition

gs.getSession().isInteractive()

Script

(function executeRule(current, previous) {

	var grContainedInSuite = new GlideRecord('scan_check_suite_check');
	var arrSuites          = [];
	
	grContainedInSuite.addQuery('check', current.getUniqueValue());
	grContainedInSuite.query();
	
	if (!grContainedInSuite.hasNext()) {
		gs.addErrorMessage(
			gs.getMessage('This Check is not contained in any Instance Scan Suite!')
		);
	}
	else {	
		arrSuites.push(
			gs.getMessage('This Check is contained in the following Instance Scan Suites:') +
			'<br/>' +
			'<ul>'
		);
		
		while (grContainedInSuite.next()) {
			var grSuite = grContainedInSuite.suite.getRefRecord();
			
			if (grSuite.isValidRecord()) {
				arrSuites.push(
					'<li>' +
					'<a href="' + grSuite.getLink() + '" target="_blank">' + 
					grSuite.getValue('name') +
					'</a>' +
					'</li>'
				)
			}
		}
		
		arrSuites.push('</ul>');

		gs.addInfoMessage(arrSuites.join(''));
	}

})(current, previous);

 

 

 

Display a warning in case an OOTB artifact has been modified

 

Changing OOTB artifacts like Script Includes or Business Rules is never a good idea, as you will get so called skipped records in the next upgrade/patch you have to care about. Otherwise, you can get into big trouble and risk broken functionalities as well as unpredictable behavior. But many developers are not aware about the consequences when playing around with OOTB artifacts or know how to revert the changes (see https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0993700 if you don't know how).

 

Therefore I thought it is helpful to display a warning when opening a modified OOTB artifact and built the following Business Rule. The warning message also provides a link to the most recent OOTB version, where you will find a UI Action for reverting to that version.

MaikSkoddow_0-1683815787006.png

Within the script I have avoided using APIs here, which are not available in Scoped Apps, so you can adopt the business rule more easily. Furthermore I have narrowed down the scope to displaying forms in he Classic UI but feel free to adopt to Workspaces or Service Portals as well.

 

 

Business Rule

 

Table

sys_metadata

Advanced

true

When

display

Condition

gs.getSession().isInteractive() && !current.isNewRecord()

Script

(function executeRule(current) {

	var _strCurrentTable = String(current.getTableName() || '');
	var _strCurrentURL   = String(gs.action.getGlideURI() || '');

	if (_strCurrentTable.length === 0) {
		return;
	}

	//only continue with form views in the Classic UI
	if (!_strCurrentURL.startsWith(_strCurrentTable + '.do')) {
		return;
	}

	//Check whether the artifact is part of an application that is connected
	//to a Git repository. If so, return as any additional check wouldn't make any sense.
    if (current.isValidField('sys_scope')) {
        var _grRepoConfig  = new GlideRecord('sys_repo_config');
        var _strScopeSysID = current.getValue('sys_scope') || 'x';

        if (_strScopeSysID !== 'x' && _grRepoConfig.get('sys_app', _strScopeSysID)) {
            return;
        }
    }

	//determine the unique artifact ID
	var _strUpdateName = 
		String(
			current.getValue('sys_update_name') || 
			new GlideUpdateManager2().getUpdateName(current) ||
			''
		);

	//Is this artifact tracked with versions?
	if (_strUpdateName.length > 0) {
		var _grUpdateXml = new GlideRecord('sys_update_xml');

		//query all versions
		_grUpdateXml.addQuery('name', _strUpdateName);
		_grUpdateXml.orderByDesc('sys_updated_on');
		_grUpdateXml.setLimit(1);
		_grUpdateXml.query();

		//the record will not be replaced in the next upgrade
		if (_grUpdateXml.next() && !_grUpdateXml.replace_on_upgrade) {
			var _grRecord            = new GlideRecord(_strCurrentTable);
			var _objUpdateVersionAPI = new global.UpdateVersionAPI();

			_grRecord.addQuery('sys_id', current.getValue('sys_id'));
			_grRecord.setLimit(1);
			_grRecord.query();

			//only continue, if it is not a custom artifact as we are only
			//interested in customized OOTB artifacts
            if (_objUpdateVersionAPI.getCustomerFileIds(_grRecord).length === 0) {
				var _isOOTB    = false;
				var _grVersion = new GlideRecord('sys_update_version');

				_grVersion.addQuery('name', _strUpdateName);
				_grVersion.orderByDesc('sys_recorded_at');
				_grVersion.query();

				//iterate the versions to check whether this is an OOTB artifact
				while (!_isOOTB && _grVersion.next()) {
					var _strSource = _grVersion.getValue('source_table') || '';

					_isOOTB = 
						_strSource === 'sys_upgrade_history' || 
						_strSource === 'sys_store_app';
				}

				if (_isOOTB) {
					gs.addErrorMessage(
						'This OOTB artifact was changed and thus will create a skipped ' +
						'record during the next upgrade!<br><br>In case the change was ' +
						'done accidentally, you should revert to the <a href="' + 
						_grVersion.getLink(true) + '" target="_blank">' +
						'most recent OOTB version.</a>'
					);
				}
			}
		}
	}

})(current);

 

 

 

Block large Profile Pictures

 

In ServiceNow users can add a picture to their personal profile record (table sys_user), which is then used as an avatar image everywhere. Basically, you probably wouldn't give it a second thought, but the problem here is that there are no size limits OOTB nor are thumbnails automatically generated from these images. However, if a larger number of these images is used somewhere (e.g. EVAM-based cards for agents to be dispatched in the FSM Dispatcher Workspace), the files are loaded in their original size, although they are actually only needed in a very small version. The consequence is a massive performance decrease due to the long loading times for all the oversized profile pictures. For this reason, it is a good idea to make sure that profile pictures do not exceed a certain file size. 

MaikSkoddow_0-1697347343762.png

 

Business Rule

 

⚠️ Please note:

The following Business Rule utilizes a System Property max_profile_picture_file_size to define the maximum file size. 

 

 

Table

sys_attachment

Advanced

true

When

before

Filter Conditions

MaikSkoddow_2-1697346995770.png

Insert

true

Script

(function executeRule(current) {

	var _numCurrentFileSize = 
		Number(current.getValue(current.compressed ? 'size_compressed' : 'size_bytes'));
	var _numMaxFileSize =
		Number(gs.getProperty('max_profile_picture_file_size', 100000));

	if (_numCurrentFileSize > _numMaxFileSize) {
		gs.addErrorMessage(
			gs.getMessage(
				'Chosen file for the profile picture is too large!' +
				' Please select a file with less than <strong>{0}</strong> bytes.',
				String(_numMaxFileSize)
			)
		);
		current.setAbortAction(true);
	}

})(current);

 

 

 

Display a warning if an artifact was skipped

 

Skipped records are a major annoyance in ServiceNow. And I am not referring to skipped records for changed OOTB artifacts, but those that arise after the deployment of custom applications via the application repository. If the processing of skipped records does not become an integral part of your deployment process, you run the risk of your instances deviating more and more from each other unnoticed and thus a flood of bug tickets is guaranteed. For this reason, the following business rule checks the sys_upgrade_history_log table when an artifact record is opened to see whether there is currently a skipped record for it and generates a corresponding warning message accordingly if this applies.

 

MaikSkoddow_0-1702276412489.png

 

 

Business Rule

Table

sys_metadata

Advanced

true

When

display

Script

(function executeRule(current, previous /*null when async*/) {

  var _strUpdateName = current.getValue('sys_update_name') || '';
  var _strCurrentURL = (gs.action.getGlideURI() || '').toString();
  
  //do nothing if records is opened in the comparator tool
  if (_strCurrentURL.indexOf('merge_form_upgrade.do') !== -1) {
    return;
  }

  if (_strUpdateName !== '') {
    var _grUpgradeHistoryLog = new GlideRecord('sys_upgrade_history_log');

    _grUpgradeHistoryLog.addQuery('file_name', _strUpdateName);
    _grUpgradeHistoryLog.orderByDesc('sys_recorded_at');
    _grUpgradeHistoryLog.setLimit(1);
    _grUpgradeHistoryLog.query();

    if (_grUpgradeHistoryLog.next()) {
      var _numDisposition  = Number(_grUpgradeHistoryLog.getValue('disposition'));
      var _changedByVendor = Number(_grUpgradeHistoryLog.getValue('changed'));
      var _strResolution   = _grUpgradeHistoryLog.getValue('resolution_status');

      if ((_numDisposition === 4 || _numDisposition === 104) && 
        _changedByVendor === 0 && 
        (_strResolution === null || _strResolution === 'not_reviewed')) 
      {
        gs.addErrorMessage(
          'It seems that on ' + 
          _grUpgradeHistoryLog.sys_recorded_at.getDisplayValue() +
          ' an update for that record was skipped. Please check the corresponding ' +
          '<a href="' + _grUpgradeHistoryLog.getLink(true) + '" target="_blank">' + 
          'Upgrade History Log entry</a> and process it accordingly.'
        );
      }
    }
  }

})(current, previous);

 

 

 

Add Relationships "Updates" & "Versions" automatically to the Form

 

Whenever you change an artifact (configuration records - not data!) you automatically create a new version (table sys_update_version) and in most of the cases also a record at table sys_update_xml which might be captured in an update set. For code reviews, I therefore always scroll to the related lists section first to check whether the "Updates" & "Versions" Relationships have been added. And I was quite tired of manually adding these two Relationships to the form every time, because they are practically nowhere to be found in a baseline version. Therefore, each time a record from a child table of sys_metadata is changed, the following Business Rule checks whether the two Relationships are present in the current view and adds them if necessary. In order not to interfer with any application the related configurations are created in the "global" scope.

 

MaikSkoddow_0-1704804819254.png

 

 

Business Rule

 

Table

sys_metadata

Advanced

true

When

before

Insert

true

Update

true

Condition

gs.getSession().isInteractive()

Script

(function executeRule(current, previous /*null when async*/) {

	var _strCurrentTable = String(current.getTableName() || '');
	var _grRelatedList   = new GlideRecord('sys_ui_related_list');
	var _arrRelatedList  = [];
	var _strView = 
		String(gs.action.getGlideURI().getMap().get('sysparm_view') || '').trim();

	if (_strCurrentTable.length === 0) {
		return;
	}

	if (_strView !== '') {
		var _grView = new GlideRecord('sys_ui_view');

		if (_grView.get('name', _strView)) {
			_strView = _grView.getUniqueValue();
		}
	}

	//if there is no view selected take the "Default" view
	if (_strView === '') {
		_strView = 'Default view';
	}

	_grRelatedList.addQuery('view', _strView);
	_grRelatedList.addQuery('name', _strCurrentTable);
	_grRelatedList.setLimit(1);
	_grRelatedList.query();

	if (!_grRelatedList.next()) {
		//no related list configuration available in the current view,
		//therefore we are going to create a new one
		_grRelatedList.initialize();

		//we will create the record in the global scope to prevent 
		//interfering with currently selected applicationn
		_grRelatedList.setValue('sys_scope', 'global');		
		_grRelatedList.setValue('name', _strCurrentTable);
		_grRelatedList.setValue('view', _strView);
		
		if (_grRelatedList.insert() === null) {
			gs.error(
			  'Could not create related list configuration for table "{0}" in view "{1}"!',
			  _strCurrentTable, _strView
			);
			return;
		}
	}
	
	var _grRelatedListEntries = new GlideRecord('sys_ui_related_list_entry');
	var _numPosition          = 0;

	_grRelatedListEntries.addQuery('list_id', _grRelatedList.getUniqueValue());
	_grRelatedListEntries.orderBy('position');
	_grRelatedListEntries.query();

	while (_grRelatedListEntries.next()) {
		var _strRelatedList = String(_grRelatedListEntries.getValue('related_list') || '');
		var _strFilter      = String(_grRelatedListEntries.getValue('filter') || '');

		if (_strRelatedList !== '' &&
			_strRelatedList !== 'REL:df364130c3110100bac1addbdfba8f78' && 
			_strRelatedList !== 'REL:67bdac52374010008687ddb1967334ee') 
		{
			_arrRelatedList.push({
				position    : _numPosition++,
				filter      : _strFilter,
				related_list: _strRelatedList
			});
		}

		_grRelatedListEntries.deleteRecord();
	}

	//add OOTB relationship "Updates"
	_arrRelatedList.push({
		position    : _numPosition++,
		filter      : '',
		related_list: 'REL:df364130c3110100bac1addbdfba8f78'
	});

	//add OOTB relationship "Versions"
	_arrRelatedList.push({
		position    : _numPosition++,
		filter      : '',
		related_list: 'REL:67bdac52374010008687ddb1967334ee'
	});

	for (var _numIndex = 0; _numIndex < _arrRelatedList.length; _numIndex++) {
		_grRelatedListEntries.newRecord();
		_grRelatedListEntries.setValue('list_id', _grRelatedList.getUniqueValue());
		_grRelatedListEntries.setValue('filter', _arrRelatedList[_numIndex].filter);
		_grRelatedListEntries.setValue('position', _arrRelatedList[_numIndex].position);
		_grRelatedListEntries.setValue(
			'related_list', _arrRelatedList[_numIndex].related_list
		);
		_grRelatedListEntries.insert();
	}

})(current, previous);

 

 

 

Warn if translations are missing for the name of the UI Action

 

In a multilingual ServiceNow instance, translations need to be added in many places and distributed across the five designated tables. One UI element where missing translations are particularly noticeable is UI Actions. As a developer, it's easy to overlook that after spending hours working on a complex UI Action, the name also needs to be translated (table sys_translated) so that the links and buttons generated on the interface will appear to users in their respective languages. The following business rule helps by displaying a warning if translations are missing for any language other than the system default language. As an additional help, the displayed language names are linked to pre-populated pages where the missing translations can be created.

 

MaikSkoddow_0-1731764723359.png

 

 

Business Rule

 

Table

sys_ui_action

Advanced

true

When

display

Condition

!current.isNewRecord()

Script

(function executeRule(current) {

	var _grLanguage = new GlideRecord('sys_language');
    var _arrList    = [];

    _grLanguage.addActiveQuery();
    _grLanguage.addQuery('id', '!=', gs.getProperty('glide.sys.language', 'en'));
    _grLanguage.query();

    while (_grLanguage.next()) {
        var _grTranslatedText = new GlideRecord('sys_translated');
        var _strLanguageID    = _grLanguage.getValue('id');
        var _strLanguageName  = _grLanguage.getValue('name');

        _grTranslatedText.addQuery('name', 'sys_ui_action');
        _grTranslatedText.addQuery('element', 'name');        
        _grTranslatedText.addQuery('language', _strLanguageID);
        _grTranslatedText.addQuery('value', current.getValue('name'));
        _grTranslatedText.query();

        if (!_grTranslatedText.hasNext()) {
            _arrList.push(
                gs.getMessage(
                    '<li>' +
						'<a href="{2}" target="_blank">' +
							'<span class="icon icon-add-circle-empty"></span> {0} ({1})' +
						'</a>' +
					'</li>', [
						_strLanguageName, 
						_strLanguageID,
						'/sys_translated.do?sys_id=-1&sysparm_query=' + 
							GlideStringUtil.urlEncode(_grTranslatedText.getEncodedQuery())
					]
                )
            );
        }
    }

	if (_arrList.length > 0) {
		gs.addErrorMessage(
			gs.getMessage(
				'<p>{0}</p><ul style="list-style-type: none;">{1}</ul>', [
				gs.getMessage(
					'Translation of the UI Action name is missing for the following languages:'
				),
				_arrList.join('')
			])
		);
	}

})(current);

 

 

 

Enforce Capturing of Scheduled Scripts for Committing to Git

 

The dictionary attribute update_synch is a key property that controls which tables have their changes tracked and captured in table sys_update_xml. If the attribute is missing or set to "false", changes to records in that table will not be captured in table sys_update_xml, the standard for raw data like incidents or locations. Tables that extend the sys_metadata table - the so-called configuration artifacts - are captured automatically unless there is a dictionary attribute update_synch=false configured for the corresponding table (and its child tables). This rare case exists for the table sysauto and all of its child tables like sysauto_script (Scheduled Script Execution). As a consequence such records cannot be committed to a Git respository as the commit sets are built based on the changes tracke in the sys_update_xml table. However with the help of Business Rule you can enforce creating a corresponding record in table sys_update_xml by leveraging the same approach like the well-know utility "Add to Update Set".

 

 

Business Rule

 

Table

sysauto_script

Advanced

true

When

after

Insert

true

Update

true

Script

(function executeRule(current) {

	var _objUpdateManager = new GlideUpdateManager2();
	
	_objUpdateManager.saveRecord(current);

})(current);

 

 

Comments
Victor Peinado1
Tera Contributor

Thanks @Maik Skoddow 

chrisperry
Giga Sage

Very useful -- thanks so much, Maik. 

 

I wanted to call attention to your "Force Creation of personal Artifacts in "Global" Scope" business rule, I think the table listed in your article is incorrect. It isn't "sys_app" but rather "sys_metadata" table.

 

Cheers and thanks again!

Maik Skoddow
Tera Patron
Tera Patron

Hi @chrisperry 

Wow, that's awesome!

Many thanks for finding that error.

There are not very many people in this world who would see that right away.

Maik

User710538
Giga Explorer

Thanks for sharing @Maik Skoddow 

yeshwanth290
Tera Contributor

Hi @Maik Skoddow 

Thanks for sharing great article. I tried your query BR for syslog table to restrict logs to 15 mins in PDI. I am always getting a blank page without any records. But I checked by direct query of last 15 mins in syslog table and I am able to see logs. Is there anything that needs to be updated from my end, Please advise.

 

Vamsi G
Giga Guru

Thanks for sharing great article. 

CFrandsen
Tera Guru

Nothing short of amazing ! 
Greatly appreciated @Maik Skoddow  !

chrisperry
Giga Sage

@Maik Skoddow might be good to replace those gs.getMessage() methods for concatenating strings with template strings that are now available to us with ES12, e.g.: `Hello ${first_name}`.

 

ServiceNow support reprimanded me on my previous project for using gs.getMessage(), as each time that method is called, servicenow executes a resource-costly query of the sys_ui_message table for translation purposes. 😀

Version history
Last update:
‎05-23-2025 12:33 AM
Updated by:
Contributors