Business Rule: Start ToC's – User & Configuration Guide

Business Rule: Start ToC's – User & Configuration Guide

Purpose: Documents the ServiceNow Business Rule that starts the SAP Transport of Copies (ToC) and the two Script Includes it depends on (TMUtils, TMAction) – including setup steps, the full scripts, and every comment/message the rule can produce.

Updated on Jul 14, 2026: Page restructured for setup order. New TMUtils Script Include added; TMAction updated to call global.TMUtils; the previous "All application scopes" step is no longer required. The earlier version is available via the page history.

1. Overview

The Start ToCs Business Rule triggers a SAP Transport of Copies when a Change Request is moved to the configured target status (customer-specific; see section 3.1). It calls global.TMAction.startToC(current), which sends the SAP OData entity call TOC_StartSet through the RTC/TM gateway service.

Two Script Includes must exist before the rule works (see section 2): TMUtils (the SAP connection, settings and OData layer) and TMAction (the action layer the rule calls).

The rule reports back to the user in two ways: an activity-log comment on the record (always), and – for real errors – a red error banner on the form plus a cancelled state change. QA checks (checkQAS) are intentionally excluded in the current version (see section 5).

2. Prerequisites

Set these up before creating the Business Rule, in this order. The dependency chain is Business Rule → TMAction → TMUtils, so build from the bottom up.

2.1 TMUtils Script Include

TMAction calls new global.TMUtils(), so create this Script Include first. It holds the SAP settings/features lookup and the OData read/write layer (getSAPTRData, setSAPTRData).

Create the Script Include – go to All → System Definition → Script Include and create New in the Global application with:

Field

Value

Field

Value

Name

TMUtils

API Name

global.TMUtils (set automatically)

Application

Global

Accessible from

This application scope only

Client callable

No

Active

Yes

📷 Screenshot: TMUtils Script Include form –

Add the following code to the Script field:

var TMUtils = Class.create(); TMUtils.metaDataPatternsEntities = { "TR_ReleaseSet": "TR_Release", "Proj_DestiSet": "Proj_Desti", "CTS_ProjectSet": "CTS_Project", "QA_TM_TR_RELSet": "QA_TM_TR_REL", "QA_TM_TRSet": "QA_TM_TR", "Transport_RequestSet": "Transport_Request", "TR_AttributeSet": "TR_Attribute", "TR_TO_TMSet": "TR_TO_TM", "TR_ReassignSet": "TR_Reassign", "TR_AssignReleaseSet": "TR_AssignRelease", "TOC_StartSet": "TOC_Start", "TR_ApproveSet": "TR_Approve", "QA_Collision": "QA_Collision" }; TMUtils.prototype = { initialize: function() { }, type: 'TMUtils', _getExternalUser(){ var gr = new GlideRecord('sys_user'); gr.addQuery('sys_id', gs.getUserID()); gr.query(); gr.next(); return "" + gr.user_name; }, _getIfPropertyExists(propertyName, entityCall){ let propertyExists = false; if (!this._metadata) { return propertyExists; } let iter = this._metadata.getChildNodeIterator(); let entityName = TMUtils.metaDataPatternsEntities[entityCall]; let nodes = 0; while(iter.hasNext() || nodes < 100) { nodes++; let n = iter.next(); if(n.getNodeName() == "EntityType" && n.getAttribute("Name") == entityName){ let iter2 = n.getChildNodeIterator(); let propertyNodes = 0; while(iter2.hasNext() || propertyNodes < 50) { propertyNodes++; let n2 = iter2.next(); if(n2.getNodeName() == "Property" && n2.getAttribute("Name") == propertyName){ propertyExists = true; break; } } break; } } return propertyExists; }, _getIfNavigationPropertyExists(propertyName, entityCall){ let propertyExists = false; if (!this._metadata) { return propertyExists; } let iter = this._metadata.getChildNodeIterator(); let entityName = TMUtils.metaDataPatternsEntities[entityCall]; let nodes = 0; while(iter.hasNext() || nodes < 100) { nodes++; let n = iter.next(); if(n.getNodeName() == "EntityType" && n.getAttribute("Name") == entityName){ let iter2 = n.getChildNodeIterator(); let propertyNodes = 0; while(iter2.hasNext() || propertyNodes < 50) { propertyNodes++; let n2 = iter2.next(); if(n2.getNodeName() == "NavigationProperty" && n2.getAttribute("Name") == propertyName){ propertyExists = true; break; } } break; } } return propertyExists; }, _getTMMessage: function (response, url) { let errorStatus = response ? response.getStatusCode() : 500; let errorMessage = "Service '" + url + "' " + response.getErrorMessage(); let isCustomMessage = false; let responseBody = {}; try{ responseBody = response.getBody(); responseBody = typeof responseBody == 'string' ? JSON.parse(responseBody) : responseBody; }catch(error){ responseBody = {}; gs.info(error.message); } // Custom SAP BTP error handling if (responseBody?.error && responseBody?.message) { errorMessage = `${String(responseBody.error)}, ${String(responseBody.message)}`; isCustomMessage = true; } else if(errorStatus != 401){ // OData-style errorMessage = responseBody && responseBody.error && responseBody.error.message ? responseBody.error.message.value : errorMessage; } return JSON.stringify({"type": "E", "status": errorStatus, "message": errorMessage || "An unknown error occurred!", "customMessage": isCustomMessage}); }, _getOdataInfo: function (entityCall, propertyName, url, authHeaders) { let odataInfo = {"x-csrf-token": "", "set-cookie": "", "propertyExists": false}; authHeaders["X-CSRF-Token"] = "FETCH"; try{ let request = new sn_ws.RESTMessageV2(); request.setEndpoint(url + "/$metadata"); request.setHttpMethod('GET'); for(let key in authHeaders){ request.setRequestHeader(key, authHeaders[key]); } let metadataResp = request.execute(); let respHeaders = metadataResp.getHeaders("set-cookie"); for(let key2 in respHeaders){ if(key2 == "set-cookie"){ odataInfo["set-cookie"] = respHeaders[key2]; } else if(key2 == "x-csrf-token"){ odataInfo["x-csrf-token"] = respHeaders[key2]; } } if(metadataResp.getStatusCode() != 200){ return odataInfo; } let responseBody = {}; try{ responseBody = metadataResp.getBody(); let xmlDoc = new XMLDocument2(); xmlDoc.parseXML(responseBody); this._metadata = xmlDoc.getFirstNode("edmx:Edmx/edmx:DataServices/Schema"); }catch(error){ responseBody = {}; gs.info(error.message); } odataInfo["propertyExists"] = this._getIfPropertyExists(propertyName, entityCall); return odataInfo; } catch(ex){ let message = ex.getMessage(); gs.info("error getting metadata" + message); return odataInfo; } }, getTMSettingsForType: function(type){ var tmSettingsRecords = new GlideRecordSecure('x_real3_app1_settings'); tmSettingsRecords.addQuery('type', type); tmSettingsRecords.query(); var updateReqTable = tmSettingsRecords.next(); var devSystems = tmSettingsRecords.getDisplayValue("dev_systems"); devSystems = devSystems ? devSystems.split(",") : []; var tmSettings = {"tmServer": "", "secure": "", "server": "", "port": "", "systems": devSystems, "ctrlName": tmSettingsRecords.getDisplayValue("tm_controller_name"), "tmEnabled": "", "tmLoginBlank": false, "loginAlias": "", "logoffAlias": "", "rtcAlias": "", "systemAlias": "", "rtcHeaders": "{}", "userLogin": ""}; var tmCtrlRecords = new GlideRecordSecure('x_real3_app1_tm_controllers'); tmCtrlRecords.addQuery('tm_controller_id', tmSettings.ctrlName); tmCtrlRecords.query(); if(tmCtrlRecords.next() && tmCtrlRecords.getDisplayValue("server")){ tmSettings.tmEnabled = tmCtrlRecords.getValue("enabled") == 1; tmSettings.tmServer = tmCtrlRecords.getValue("secure") == 1 ? "https://" : "http://"; tmSettings.tmServer = tmSettings.tmServer + tmCtrlRecords.getDisplayValue("server"); tmSettings.secure = tmCtrlRecords.getValue("secure") == 1; tmSettings.server = tmCtrlRecords.getDisplayValue("server"); tmSettings.port = tmCtrlRecords.getDisplayValue("port"); tmSettings.tmLoginBlank = tmCtrlRecords.getValue("tm_login_new_tab") == 1; tmSettings.loginAlias = tmCtrlRecords.getDisplayValue("login_service_alias"); tmSettings.logoffAlias = tmCtrlRecords.getDisplayValue("logoff_service_alias"); tmSettings.rtcAlias = tmCtrlRecords.getDisplayValue("rtc_service_alias"); tmSettings.systemAlias = tmCtrlRecords.getDisplayValue("system_alias"); var rtcHeaders = tmCtrlRecords.getDisplayValue("rtc_headers") || "{}"; tmSettings.rtcHeaders = JSON.parse(rtcHeaders); tmSettings.userLogin = tmCtrlRecords.getValue("user_login") == 1; var port = tmCtrlRecords.getDisplayValue("port"); if(port){ tmSettings.tmServer = tmSettings.tmServer + ":" + port; } } return tmSettings; }, getTMFeaturesForType: function(type){ var tmFeaturesRecords = new GlideRecordSecure('x_real3_app1_features'); tmFeaturesRecords.addQuery('type', type); tmFeaturesRecords.query(); var tmFeatures = {"uiConfig": {}, "creation": {}, "approval": {}, "reassign": {}, "release": {}, "prodRelease": {}, "toc": {}}; if(tmFeaturesRecords.next()){ var toc = tmFeaturesRecords.getDisplayValue("toc") || ""; var prod_release = tmFeaturesRecords.getDisplayValue("prod_release") || ""; var ui_config = tmFeaturesRecords.getDisplayValue("ui_config") || ""; tmFeatures.uiConfig = ui_config ? JSON.parse(ui_config) : {}; tmFeatures.creation = JSON.parse(tmFeaturesRecords.getDisplayValue("creation") || "{}"); tmFeatures.approval = JSON.parse(tmFeaturesRecords.getDisplayValue("approval") || "{}"); tmFeatures.reassign = JSON.parse(tmFeaturesRecords.getDisplayValue("reassign") || "{}"); tmFeatures.release = JSON.parse(tmFeaturesRecords.getDisplayValue("release") || "{}"); tmFeatures.prodRelease = prod_release ? JSON.parse(prod_release) : {}, tmFeatures.toc = toc ? JSON.parse(toc) : {}; } return tmFeatures; }, getSAPTRData: function(req){ req = req || this.getParameter("sysparm_req"); req = JSON.parse(req) || {}; let entityCall = req.entityCall; let query = req.query; let connectDetails = req.connectDetails || {}; let tmServer = connectDetails.tmServer; let path = connectDetails.path; let sapId = req.sapId; let authHeaders = connectDetails.rtcHeaders || {}; if(connectDetails.sapUserLogin){ let session = gs.getSession(); let sapCookies = session.getClientData("SAPLogonCookies_"+ sapId); if(sapCookies && sapCookies.length){ authHeaders["Cookie"] = sapCookies; } } //FETCH odata info ["x-csrf-token", "set-cookie", "propertyExists"] let odataInfo = this._getOdataInfo(entityCall, "ExternalUser", tmServer + path, authHeaders); if(entityCall == "TR_AttributeSet"){ entityCall = "Transport_RequestSet('"+ query.trkorr +"')/TR_AttributeSet"; query = ""; } else if (entityCall == "Proj_DestiSet") { let odataInfoTRShort = this._getIfPropertyExists("TrShortDescr", entityCall); query = odataInfoTRShort ? query.path + ` and TrShortDescr eq '${query.currentShortText}'` : query.path; } if(query && entityCall != "QA_TM_TR_RELSet" && entityCall != "QA_TM_TRSet"){ query = "?$filter=" + encodeURIComponent(query); } if (odataInfo["propertyExists"]) { let externalUser = this._getExternalUser(); query = query ? query + encodeURIComponent(" and ") : "?$filter="; query = query + encodeURIComponent("ExternalUser eq '" + externalUser + "'"); } if(entityCall == "QA_TM_TRSet" && query.includes('{navToCollObj}')){ let navToCollObj = this._getIfNavigationPropertyExists("NavToCollisionObjects", "QA_Collision"); if(navToCollObj){ query = query.replace("{navToCollObj}", "NavToCollisionObjects"); } else { query = query.replace("/{navToCollObj}", ""); } } try{ let request = new sn_ws.RESTMessageV2(); request.setEndpoint(tmServer + path + "/" + entityCall + query); request.setHttpMethod('GET'); request.setRequestHeader("Accept", "application/json"); for(let key in authHeaders){ request.setRequestHeader(key, authHeaders[key]); } let response = request.execute(); let responseBody = {}; try{ responseBody = response.getBody(); responseBody = typeof responseBody == 'string' ? JSON.parse(responseBody) : responseBody; }catch(error){ responseBody = {}; gs.info(error.message); } if(response.getStatusCode() != 200){ return this._getTMMessage(response, tmServer + path + "/" + entityCall); } else { responseBody = typeof responseBody == "string" ? JSON.parse(responseBody) : (responseBody || {}); return JSON.stringify(responseBody.d ? responseBody.d.results : []); } } catch(ex){ let errorMessage = ex.getMessage(); gs.info(errorMessage); return JSON.stringify({"type": "E", "status": 500, "message": errorMessage || "An unknown error occurred!"}); } }, setSAPTRData: function(connect_details, entity_call, req_body){ let connectDetails = connect_details || this.getParameter("sysparm_connect_details"); connectDetails = JSON.parse(connectDetails); let entityCall = entity_call || this.getParameter("sysparm_entity_call"); let requestBody = req_body || this.getParameter("sysparm_req_body"); requestBody = requestBody.startsWith("{") ? JSON.parse(requestBody) : requestBody; let authHeaders = connectDetails.rtcHeaders || {}; let tmServer = connectDetails.tmServer; let path = connectDetails.path; let reqType = "POST"; let username = "SUCCESS"; let serviceQuery = ""; let externalUser = this._getExternalUser(); if(connectDetails.sapUserLogin){ let session = gs.getSession(); let sapCookies = session.getClientData("SAPLogonCookies_"+ connectDetails.sapId); if(sapCookies && sapCookies.length){ authHeaders["Cookie"] = sapCookies; username = session.getClientData("SAPLogonCookieUSER_"+ connectDetails.sapId); } } //FETCH odata info ["x-csrf-token", "set-cookie", "propertyExists"] if(entityCall == "massProdRelease"){ entityCall = "TR_AssignReleaseSet"; } else if(entityCall == "massApproval" || entityCall == "massRevokal"){ entityCall = "TR_ApproveSet"; serviceQuery = requestBody.requests; requestBody = requestBody.body; } else if(entityCall == "TR_ApproveSet"){ requestBody['UserName'] = ""; serviceQuery = "(Trkorr='" + requestBody.trkorr + "')"; } else if(entityCall == "Proj_DestiSet_Reassign"){ entityCall = "Proj_DestiSet"; serviceQuery = requestBody.serviceQuery; requestBody = requestBody.body; } let odataInfo = this._getOdataInfo(entityCall, "ExternalUser", tmServer + path, authHeaders); authHeaders["X-CSRF-Token"] = odataInfo["x-csrf-token"]; if(entityCall == "massRelease"){ authHeaders["Content-Type"] = "multipart/mixed;boundary=batch_test"; entityCall = "$batch"; //from url path it's mandatory requestBody = requestBody.replaceAll(",ExternalUser='$externalUser'", ",ExternalUser='"+ externalUser +"'"); if(odataInfo["propertyExists"]){ requestBody = requestBody.replaceAll("$externalUser", externalUser); } else { requestBody = requestBody.replaceAll(',\n"ExternalUser":"$externalUser"', ''); } } else { //update request if (["TR_TO_TMSet", "TR_ReassignSet", "TR_AssignReleaseSet", "TR_ApproveSet", "Proj_DestiSet"].includes(entityCall)) { reqType = "PUT"; if(entityCall == "TR_TO_TMSet"){ if(odataInfo["propertyExists"]){ entityCall = "TR_TO_TMSet(TransportReq='" + requestBody.trkorr + "',ExternalUser='" + externalUser + "',CrudMode='C')"; } else { entityCall = "TR_TO_TMSet(TransportReq='" + requestBody.trkorr + "',CrudMode='C')"; } } else if(entityCall == "TR_AssignReleaseSet"){ entityCall = "TR_AssignReleaseSet(RelId='" + requestBody.prodReleaseID + "')"; delete requestBody["prodReleaseID"]; } else if(entityCall == "TR_ReassignSet"){ entityCall = "TR_ReassignSet('" + requestBody.trkorr + "')"; } delete requestBody.trkorr; } if(odataInfo["propertyExists"]){ requestBody["ExternalUser"] = externalUser; } authHeaders["Accept"] = "application/json"; authHeaders["Content-Type"] = "application/json"; requestBody = JSON.stringify(requestBody); } if(odataInfo["set-cookie"] && !connectDetails.sapUserLogin){ let cookies = odataInfo["set-cookie"].split("; "); let cookie = ""; for(let i = 0; i < cookies.length; i++){ if(cookies[i].includes("sap-usercontext=") || cookies[i].includes("SAP_SESSIONID")){ cookies[i] = cookies[i].replace("path=/", ""); cookies[i] = cookies[i].replace(",", ""); cookie = i ? cookie + "; " + cookies[i].trim() : cookies[i].trim(); } } authHeaders["Cookie"] = cookie; } try{ let request = new sn_ws.RESTMessageV2(); request.setEndpoint(tmServer + path + "/" + entityCall + serviceQuery); request.setHttpMethod(reqType); request.setRequestBody(requestBody); for(let key in authHeaders){ request.setRequestHeader(key, authHeaders[key]); } let response = request.execute(); let statusCode = response.getStatusCode(); gs.info("Update status code" + statusCode); if(statusCode >= 400 || response.getErrorMessage()){ return this._getTMMessage(response, tmServer + path + "/" + entityCall); } else if(statusCode == 202){ return "Accepted"; } else if(reqType == "PUT"){ return username; } else { let responseBody = {}; try{ responseBody = response.getBody(); responseBody = typeof responseBody == 'string' ? JSON.parse(responseBody) : responseBody; }catch(error){ responseBody = {}; gs.info(error.message); } return JSON.stringify(responseBody ? responseBody.d : {}); } } catch(ex){ let errorMessage = ex.getMessage(); gs.info(errorMessage); return JSON.stringify({"type": "E", "status": 500, "message": errorMessage || "An unknown error occurred!"}); } } };

2.2 TMAction Script Include

The Business Rule calls new global.TMAction(). TMAction in turn uses global.TMUtils (from 2.1), so create it after TMUtils.

Create the Script Include – go to All → System Definition → Script Include and create New in the Global application with:

Field

Value

Field

Value

Name

TMAction

API Name

global.TMAction (set automatically)

Application

Global

Accessible from

This application scope only

Client callable

No

Active

Yes

📷 Screenshot: TMAction Script Include form –

Add the following code to the Script field:

var TMAction = Class.create(); TMAction.prototype = { initialize: function() { }, type: 'TMAction', approveRequests: function(current, approvalConfig){ let taskId = "" + current.sys_id; let taskNumber = "" + current.number; let type = current.getRecordClassName(); //get controller let tmUtils = new global.TMUtils(); let tmSettingsDetails = tmUtils.getTMSettingsForType(type); if(tmSettingsDetails.tmEnabled){ let tmServerValue = tmSettingsDetails.tmServer; let connectDetails = { "tmServer": tmServerValue, "loginAlias": tmSettingsDetails.loginAlias || "/sap/bc/gui/sap/its/webgui", "logoffAlias": tmSettingsDetails.logoffAlias || "/sap/public/bc/icf/logoff", "sapUserLogin": tmSettingsDetails.sapUserLogin, "rtcHeaders": tmSettingsDetails.rtcHeaders, "path": tmSettingsDetails.rtcAlias || "/sap/opu/odata/RTC/TM_GW_SRV", "sapId": type, "ExternalUser": gs.getUserName() }; if (tmSettingsDetails.systemAlias) { connectDetails["path"] = connectDetails["path"] + ";o=" + tmSettingsDetails.systemAlias; } let tmFeatures = tmUtils.getTMFeaturesForType(type); let itsmName = tmFeatures["creation"]["itsmName"] || ""; let entityCall = approvalConfig.approve == 'A' ? "massApproval" : "massRevokal"; approvalConfig.group = approvalConfig.group || ""; let requestNumbers = this.getSAPTrsForApproval(taskId, approvalConfig); let requestBody = { "requests": `(Trkorr='${requestNumbers}')`, "body": { "Approve": approvalConfig.approve, "ApprovalType": approvalConfig.type, "LevelName": approvalConfig.levelName } }; if (approvalConfig.group) { requestBody.body['Usrgroup'] = approvalConfig.group; } let response = tmUtils.setSAPTRData(JSON.stringify(connectDetails), entityCall, JSON.stringify(requestBody)); return response; } }, checkQAS: function(current){ let taskId = current.sys_id; let type = current.getRecordClassName(); //get controller let tmUtils = new global.TMUtils(); let tmSettingsDetails = tmUtils.getTMSettingsForType(type); if(tmSettingsDetails.tmEnabled){ var tmServerValue = tmSettingsDetails.tmServer; //call get SAP QAS let trId = this.getSAPTrs(taskId); let query = "?$expand=NavToCritObj,NavToSpecialAspects&$filter=Trkorr" + encodeURIComponent(" eq'" + trId +"' and Toc eq 'X'"); let connectDetails = { "tmServer": tmServerValue, "loginAlias": tmSettingsDetails.loginAlias || "/sap/bc/gui/sap/its/webgui", "logoffAlias": tmSettingsDetails.logoffAlias || "/sap/public/bc/icf/logoff", "sapUserLogin": tmSettingsDetails.sapUserLogin, "rtcHeaders": tmSettingsDetails.rtcHeaders, "path": tmSettingsDetails.rtcAlias || "/sap/opu/odata/RTC/TM_GW_SRV", "sapId": type, "ExternalUser": gs.getUserName() }; if (tmSettingsDetails.systemAlias) { connectDetails["path"] = connectDetails["path"] + ";o=" + tmSettingsDetails.systemAlias; } let request = {"entityCall": "QA_TM_TR_RELSet", "query": query, "connectDetails": connectDetails}; let response = tmUtils.getSAPTRData(JSON.stringify(request)); return response; } }, addComment: function(current, text){ let taskId = current.sys_id; let type = current.getRecordClassName(); let task = new GlideRecordSecure(type); if (task.get(taskId)) { task.comments = text; task.update(); } }, startToC: function(current){ let taskId = "" + current.sys_id; let taskNumber = "" + current.number; let type = current.getRecordClassName(); //get controller let tmUtils = new global.TMUtils(); let tmSettingsDetails = tmUtils.getTMSettingsForType(type); if(tmSettingsDetails.tmEnabled){ let tmServerValue = tmSettingsDetails.tmServer; let connectDetails = { "tmServer": tmServerValue, "loginAlias": tmSettingsDetails.loginAlias || "/sap/bc/gui/sap/its/webgui", "logoffAlias": tmSettingsDetails.logoffAlias || "/sap/public/bc/icf/logoff", "sapUserLogin": tmSettingsDetails.sapUserLogin, "rtcHeaders": tmSettingsDetails.rtcHeaders, "path": tmSettingsDetails.rtcAlias || "/sap/opu/odata/RTC/TM_GW_SRV", "sapId": type, "ExternalUser": gs.getUserName() }; if (tmSettingsDetails.systemAlias) { connectDetails["path"] = connectDetails["path"] + ";o=" + tmSettingsDetails.systemAlias; } let tmFeatures = tmUtils.getTMFeaturesForType(type); let itsmName = tmFeatures["creation"]["itsmName"] || ""; let bodyReq = { "ItsmId": itsmName, "TicketId": taskNumber, "TicketGuid": taskId, "Trkorr": "" }; let response = tmUtils.setSAPTRData(JSON.stringify(connectDetails), "TOC_StartSet", JSON.stringify(bodyReq)); return response; } }, checkAllImported: function(current, level, state){ let sapId = "" + current.sys_id; //check SAP transports import let imported = false; let reqTable = new GlideRecordSecure('x_real3_app1_sap_transports'); reqTable.get(sapId); let taskId = reqTable.item_sys_id; let reqTable2 = new GlideRecordSecure('x_real3_app1_sap_transports'); reqTable2.addQuery('item_sys_id', taskId); reqTable2.addQuery('type_technical', "T"); reqTable2.query(); while(reqTable2.next()){ let imports = reqTable2.getValue("imports") || []; imports = typeof imports == "string" ? JSON.parse(imports) : imports; for(let i = 0; i < imports.length; i++){ if(imports[i]["TM_LEVEL"] == level){ let rc = "" + imports[i]["RETURN_CODE"]; rc = rc.trim(); imported = rc.length > 0; } } } gs.info("IMPORTED end:" +imported); //update incident if(imported){ gs.info("UPDATED"); let gr = new GlideRecord('task'); gr.get(taskId); gr.state = state; gr.update(); } }, getSAPTrs: function(sys_id){ let sapTrs = ""; let reqTable = new GlideRecordSecure('x_real3_app1_sap_transports'); reqTable.addQuery('item_sys_id', sys_id); reqTable.addQuery('type_technical', "!=", "T"); reqTable.query(); while(reqTable.next()){ if(sapTrs.length){ sapTrs = sapTrs + ","; } sapTrs = sapTrs + reqTable.getValue("transport_no"); } return sapTrs; }, getSAPTrsForApproval: function(sys_id, approvalConfig){ let sapTrs = ""; let reqTable = new GlideRecordSecure('x_real3_app1_sap_transports'); reqTable.addQuery('item_sys_id', sys_id); reqTable.addQuery('tm_level_technical', approvalConfig.levelName); if(approvalConfig.approve == 'A'){ reqTable.addQuery('status_technical', "S"); } else { reqTable.addQuery('status_technical', "!=", "Q"); } reqTable.query(); let approvals = []; while(reqTable.next()){ let sapTr = reqTable.getValue("transport_no"); let approvalsTech = []; if(approvalConfig.type == "C"){ approvalsTech = reqTable.getValue("content_approvals_tech") ? JSON.parse(reqTable.getValue("content_approvals_tech")) : []; } else if(approvalConfig.type == "T"){ approvalsTech = reqTable.getValue("technical_approvals_tech") ? JSON.parse(reqTable.getValue("technical_approvals_tech")) : []; } for(let i = 0; i < approvalsTech.length; i++){ if((approvalConfig.approve == 'A' && !approvalsTech[i]["SIGN_USER"]) || (approvalConfig.approve == 'R' && approvalsTech[i]["SIGN_USER"])){ approvalsTech[i]["GROUP"] = approvalsTech[i]["GROUP"] || ""; if(approvalConfig.group != approvalsTech[i]["GROUP"]){ continue; } if(sapTrs.length){ sapTrs = sapTrs + ","; } sapTrs = sapTrs + sapTr; break; } } } return sapTrs; } };

3. Create the Business Rule “Start ToCs”

3.1 Business Rule configuration

Create the Business Rule – go to All → System Definition → Business Rules and create New in the Global application:

  • Select the Table (Change Request).

  • Check Advanced.

  • On the When to run tab, set the values below.

  • On the Advanced tab, add the script from section 3.2 – enable ECMAScript 2021 (ES12) mode.

Setting

Value

Setting

Value

Name

Start ToCs

Table

Change Request [change_request]

Application

Global

Active

Yes

Advanced

Yes

When

before

Order

100

Insert / Update / Delete / Query

Update only

Filter Condition

State is <customer target status>

 

Note: The target status is customer-specific. To be Tested is only an example – replace it with the status used in the customer's ServiceNow instance.

Key requirement: The rule runs before Update by design. current.setAbortAction(true) only prevents the state change in a before rule – in an after rule the new state is already saved and cannot be held back.

3.2 Script

Add this script on the Advanced tab of the Business Rule.

(function executeRule(current, previous /*null when async*/) { try{ let tmAction = new global.TMAction(); /* --- QA-Checks excluded --- let qas = tmAction.checkQAS(current); qas = typeof qas == "string" ? JSON.parse(qas) : qas; if(qas.type == "E"){ tmAction.addComment(current, qas.message); } else if(qas.length){ if(qas[0]["HasCollisions"] || qas[0]["HasCriticalObj"] || qas[0]["HasDependency"] || qas[0]["HasSpecialAspects"]){ qas.shift(); tmAction.addComment(current, JSON.stringify(qas)); } } --- End of QA-Checks --- */ let response = tmAction.startToC(current); // start Transport of Copies response = typeof response == "string" ? JSON.parse(response) : response; if(response.type == "E"){ //tmAction.addComment(current, response.message); // --- TOC failed > error returned by SAP --- // "No data found" = no modifiable transports -> not a real error if(response.message && response.message.indexOf("No data found") > -1){ // informational only: nothing to transport, allow the state change tmAction.addComment(current, "ℹ️ ToC start skipped: no modifiable transports found."); } else { // real error -> keep state, inform user, abort let msg = "❌ ToC start failed (SAP response): " + response.message + " – State was NOT changed."; tmAction.addComment(current, msg); gs.addErrorMessage(msg); // red banner on the form current.setAbortAction(true); // prevent state change on error } } else { // TOC started successfully tmAction.addComment(current, "✅ ToC(s) successfully started."); } } catch(error){ gs.addErrorMessage(error); //add error message for user current.setAbortAction(true); //abort state change action } })(current, previous);

4. Behaviour and messages

Case

Condition

Comment written

Banner

State change

Case

Condition

Comment written

Banner

State change

Success

type not "E"

✅ ToC(s) successfully started.

proceeds

No modifiable transports

"E" + message contains "No data found"

ℹ️ ToC start skipped: no modifiable transports found.

proceeds (info, not an error)

Real SAP error

"E" (other message)

❌ ToC start failed (SAP response): <SAP text> – State was NOT changed.

red banner (same text)

aborted

Exception

catch

error message

aborted

Why the prefix text? The message text itself already comes from SAP (e.g. Not all transport tasks were released (ex: SBDK901112)) and is shown verbatim. We deliberately add the ToC start failed (SAP response): prefix so that this SAP feedback is clearly visible and recognisable as surfaced by our rule.

Comment vs. abort: addComment persists via its own GlideRecord.update(), so the comment is saved even when the state change is aborted.

5. Optional: Enable QA checks

The checkQAS block (SAP entity QA_TM_TR_RELSet) is currently commented out. It only reads the QA/release status (collisions, critical objects, dependencies, special aspects) and, when active, writes a comment.

  • To re-enable QA checks: remove the /* --- QA checks excluded --- ... --- end QA checks --- */ wrapper.

  • If never needed: the block can be deleted entirely; the comment wrapper only exists for quick re-activation.

checkQAS is a read-only check and does not release or approve any transport.