How to Script a change Risk Assessment
We have a couple workflows that open change requests. Part of our updated change management process is that Normal changes require a Risk Assessment be completed. We didn't want to deviate from that rule no matter what was opening the CR, so we needed a way to script the risk assessment and attach it to the CR. And we figured APIs would probably use it so we wanted a way to pass a JSON payload of the metric questions and responses to it as well.
This is how we did it server side. If you need a client side version you'll need to modify this script include or create a second one to pass over.
This does take some coordination with whomever is managing your Risk Assessments to not go in and change the name of the metric questions, the definition display value, or the definition value (asmt_metric_definition) without getting this script include updated.
Created a script include called "u_changeAPIUtils" (you can call yours whatever you want). Client callable: False
Accessible from: All application scopes (not super sure this is necessary)
- We select which risk assessment to take.
- If a JSON payload was passed in, we make sure we have matching values to any display_values otherwise we set the "default values" as the JSON payload (this makes it easier later).
- Create the assessment group (asmt_assessment).
- Create the assessment instance (asmt_assessment_instance).
- Lookup the risk assessment questions (asmt_metric).
- For each question on the risk assessment,
- 1. Create a metric result (asmt_metric_result) record using the JSON payload.
- 1. Remember, we normalized the JSON payload or set a default JSON payload earlier.
- If we have a metric question (asmt_metric) that matches an object in our payload, use the value we determined earlier as the actual_value.
- If we don't have a matching JSON object, we assume the smallest possible value. This is just a cover if someone added a new question to the risk assessment that we did not have ready in our payload.
- If we have a metric question (asmt_metric) that matches an object in our payload, use the value we determined earlier as the actual_value.
- Create an instance question record (asmt_assessment_instance_question), copying what we did for the metric result question.
- 1. Remember, we normalized the JSON payload or set a default JSON payload earlier.
- Go back to the assessment instance and change it to complete.
- Done!
var u_ChangeAPIUtils = Class.create();
u_ChangeAPIUtils.prototype = {
initialize: function() {},
/* Create and submit a risk assessment so risk condtiions will run and the assessment can be taken again if the CR goes back to New. And so the state transitions won't prevent the CR from being submitted.
* current: the GlideObject of the change request
* metricObj: an optional JSON object of the risk assessment questions and answers */
submitRiskAssessment: function(cr, metricObj) {
this.chg_risk_asmt = 'sys_id of the Risk Assessment to be take'; //The metric_type on change_risk_asmt. You could pass this as an arguement to the function as well.
/* Step 0)
* If a JSON payload of the metric names with the values was not passed, then we create a default version.
* If a JSON payload was passed, we verify we have a value in the object to pair up with any display_value passed. */
if (!metricObj) { //If a metricObj wasn't passed, we create a default one instead.
metricObj = {
//The object name needs to be the asmt_metric.name field (on the question). You could change this to the exact question instead, but may encounter issues with special characters. And you'll need to change the update below (the if (metricObj[metric_qs.name])) to go against the other field instead. Name isn't ever displayed to users so that is why we picked that.
//This list will need to be updated anytime a question is added, removed, or the display_value or actual_value is changed. I suppose you could store this somewhere so you're not editing a script include every time.
"Complexity": {
"display_value": 'Not complex',
"value": 1
},
"Critical CIs or services affected": {
"display_value": 'No',
"value": 1
},
"Outage required": {
"display_value": 'No outage',
"value": 1
},
"Verification": {
"display_value": 'Not difficult',
"value": 1
},
"Backout difficulty": {
"display_value": "Not difficult",
"value": 1
},
"Compliance and regulated data": {
"display_value": "No",
"value": 1
}
};
} else {
//We don't know for sure what was passed in. It could contain only display_values, so we go find the values associated to the display value and assign them now. Let's normalize!
metricObj = this._assignRiskValuesToDisplay(metricObj);
}
/* Step 1)
* We need to create the asmt_assessment. This is needed to link to the assessment instance. It is also where the metric results (answered questiosns, I think) will get attached to. */
var asmt_group = new GlideRecord('asmt_assessment'); //This is the Assessment group when looking at an assessment instance.
asmt_group.initialize();
asmt_group.metric_type = this.chg_risk_asmt;
asmt_group.insert();
/* Step 2)
* Next we create the assessment instance that gets related to the assement group. */
var instance = new GlideRecord('asmt_assessment_instance');
instance.initialize();
instance.metric_type = this.chg_risk_asmt;
instance.assessment_group = asmt_group.sys_id;
instance.user = cr.assigned_to.sys_id; //Using the sys_id causes a unique key violation for unknown reasons.
//If these aren't completed it generates a unique key violation error.
var laterDate = new GlideDate();
laterDate.addDaysUTC(14);
instance.expiration_date = laterDate;
instance.due_date = new GlideDate();
instance.taken_on = new GlideDateTime();
instance.state = 'wip'; //We come back and update this later. If we don't save it as Work in Progress first it generates a unique key violation.
instance.task_id = cr.sys_id;
instance.insert();
/* Step 3)
* Now for the gross part. We have to go all the way down into the questions that are on the change_risk_asmt (sys_id from earlier) and start working our way up with values. Oh, and we need to do it in 2 spots or the questions won't be visible if someone were to retake the risk assessment. */
//We create a blanket value of all of the questions set to 1. This is incase an asmt_metric question get renamed (boo!) or a new one is added that we're not skipping questions.
//Get the metric (the questions) where the type is the change_risk_asmt.
var metric_qs = new GlideRecord('asmt_metric');
metric_qs.addQuery('category.metric_type', this.chg_risk_asmt);
metric_qs.query();
while (metric_qs.next()) {
/* Step 4)
* For each metric question we need to create a matching metric result. This is where we use the JSON payload to specify what value gets set for what question. */
var m_result = new GlideRecord('asmt_metric_result'); //You get to it through the asmt_group
m_result.initialize();
m_result.metric = metric_qs.sys_id;
m_result.instance = instance.sys_id;
m_result.assessment_group = asmt_group.sys_id;
m_result.user = cr.assigned_to.sys_id;
//We assume that at some point somoene will rename or add a question and the JSON above won't be updated.
//If the JSON payload does have a matching value by name (or you could call something out specifically instead), we set it to the value we determined earlier.
//If the JSON payload does *not* have a match by name (meaning the question changes or is new), we just assume the lowest value instead. Better to add a default value than to have an empty question. And if it isn't in your case, then move this IF statement above the "var m_result" and wrap it to the end of the while prevent "undefined" from showing up (and probably a unique key violation)
if (metricObj[metric_qs.name]) {
m_result.actual_value = metricObj[metric_qs.name].value;
} else {
m_result.actual_value = 1; //We use 1 as the lowest value. Adjust for your risk assessment as needed.
}
m_result.insert();
/* Step 5)
* We need to create a record on asmt_assessment_instance_question too! If we don't create this second one here then when you try to do the Risk Assesment again it doesn't show the questions.
* We just create the record and pretty much copy what we just did above. */
var insta_question = new GlideRecord('asmt_assessment_instance_question');
insta_question.initialize();
insta_question.metric = metric_qs.sys_id;
insta_question.instance = instance.sys_id;
insta_question.category = metric_qs.category.sys_id;
insta_question.source_table = 'change_request';
insta_question.source_id = cr.sys_id;
insta_question.value = m_result.actual_value;
insta_question.insert();
} //End of metric_qs while
/* Step 6)
* Update the instance to 'complete' and we're done! */
instance.state = 'complete'; //If we don't come back later for this it results in errors.
instance.update();
return instance.number;
},
/* If a metricObj was passed for the Risk Assessment, make sure there is a value paired with the display_value. */
_assignRiskValuesToDisplay: function(metricObj) {
//If our JSON object only has display values and no values, assign the appropriate values now. This is way easier to do now then attempting to pair them up once we start creating records.
for (var v in metricObj) {
if (metricObj[v].value) { //Skip it if there is a value. We may update this later to verify the value is valid.
continue;
}
var metric_def = new GlideRecord('asmt_metric_definition');
metric_def.addQuery('metric.category.metric_type.sys_id', this.chg_risk_asmt);
metric_def.addQuery('metric.name', v);
metric_def.addQuery('display', metricObj[v].display_value);
metric_def.query();
if (metric_def.next()) {
metricObj[v].value = metric_def.value.toString();
}
}
return metricObj;
},
type: 'u_ChangeAPIUtils'
};
Then you just call it like this
new global.u_ChangeAPIUtils().submitRiskAssessment(cr, payload); //Or don't include a payload.
Hope this is helpful!
Labels:
https://www.servicenow.com/community/developer-articles/how-to-script-a-change-risk-assessment/ta-p/2318654
