5 ITSM solutions to implement today in Jira Cloud
Here are 5 example scripts tailored to address some of the most common ITSM use cases: from automating safety checks with parent issues, to updating lists of requirements in Assets.
Fancy a trip to the circus? Let us transport you to the world of an international circus enterprise currently navigating the many hurdles of tour season.
On top of the hustle of coordinating multi-city shows, this circus has to ensure compliance with rigorous security, safety, and meet HR protocols for performers, equipment, transportation, and venues. Sound like familiar challenges?
Here are 5 example scripts tailored to address some of these common ITSM--sorry, circus--use cases, from automating safety checks with parent issues, to updating lists of requirements in Assets, and other everyday service management tasks in Jira Cloud.
1. “As a major international circus we run many different tours at the same time with performers from around the world.”
This requires a support portal for performers to have the possibility to request the HR team to assist them with visas, and travel arrangements. For that, the company requires to create a customer portal that allows performers to interact with HR both prior to joining the circus and whilst on tour.
Proposed solution: Add comments to issues created through the JSM customer portal
Implementation with ScriptRunner for Jira Cloud: Allocate checks using round-robin based on the location of the performer.
You can use the code snippet below to implement a similar solution, and visit our documentation for other Script Listeners script examples.
Add comments to issues created through the customer portal
def comment = """
Hi, ${issue.fields.reporter.displayName} from ${issue.fields.customfield_12818.value} office,
Thank you for creating this ticket in our service desk. You have requested a laptop replacement delivered to following destination:
${issue.fields.customfield_12831}
Please make sure the address is correct. We will respond to your request shortly.
Kindly also note if the ticket remains inactive for a period of 10 days then will automatically be closed.
"""
def addComment = post("/rest/servicedeskapi/request/${issue.key}/comment")
.header('Content-Type', 'application/json')
.body([
body: comment,
// Make comment visible in the customer portal
public: true,
])
.asObject(Map)
assert addComment.status >= 200 && addComment.status <= 300
2. “Full records of servicing and safety checks need to be kept and they need to be readily available for the organisation.”
To ensure optimal efficiency in this process, the company must notify all pertinent staff members regarding tour items requiring inspection and record the outcomes of each security check.
Proposed solution: To create, on a weekly schedule, a parent Issue for all checks to be completed in each tour location.
Implementation with ScriptRunner for Jira Cloud: Create subtasks when the parent Issue is created.
You can use the code snippet below and this example script to implement a similar solution.
Note that in the code below, a customer selects a Model (Assets field) in the customer portal. The AQL query is then performed on the model and matched phones from the query results will be filled into another Assets field named “Phones”.
Update Assets field from AQL based on another Assets field value
final MODEL_ASSETS_CUSTOMFIELD_ID = 'customfield_12832'
final PHONES_ASSETS_CUSTOMFIELD_ID = 'customfield_12834'
final ATTRIBUTE_NAME_OF_MODEL_IN_PHONES = 'Model Name'
def issueKey = issue.key
def getModelNameResponse = get('/rest/api/3/issue/' + issueKey)
.header('Content-Type', 'application/json')
.queryString("expand", "${MODEL_ASSETS_CUSTOMFIELD_ID}.cmdb.attributes")
.asObject(Map)
assert getModelNameResponse.status == 200
if (!getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID]) {
logger.warn "No value in Assets field: $MODEL_ASSETS_CUSTOMFIELD_ID"
return
}
def workspaceId = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().workspaceId
def objectSchemaId = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().objectType.objectSchemaId
def modelName = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().label
def aql = """
"objectSchemaId" = "$objectSchemaId" and "$ATTRIBUTE_NAME_OF_MODEL_IN_PHONES" = "$modelName"
"""
def aqlQueryResponse = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
.header('Content-Type', 'application/json')
.basicAuth("user@example.com", "<api_token>")
.body([
qlQuery: aql
])
.asObject(Map)
assert aqlQueryResponse.status == 200
def matchedPhoneAssetIds = aqlQueryResponse.body.values*.globalId
def updatePhonesAssetsFieldResponse = put('/rest/api/3/issue/' + issueKey)
.header('Content-Type', 'application/json')
.body([
fields:[
(PHONES_ASSETS_CUSTOMFIELD_ID): matchedPhoneAssetIds.collect { id -> [id: id] }
]
]).asString()
assert updatePhonesAssetsFieldResponse.status == 204
3. “As there is a large amount of equipment and other assorted items, this information needs to be easily accessed at all times.”
For a regular scheduled maintenance to be executed, a new ticket needs to be created and added to the backlog each time, and this would require to always know which equipment is due a safety inspection.
Proposed solution: On a schedule, update the list of equipment stored in Assets to indicate the next inspection due.
Implementation with ScriptRunner for Jira Cloud: Flag any equipment that fails an inspection for secure disposal.
You can use the example code snippet below to implement a similar solution or copy it directly from our script library here.
Update the list of equipment stored in Assets to indicate the next inspection due
import java.time.format.DateTimeFormatter
import java.time.LocalDate
// To find the Assets Attribute ID. Navigating to Scheme > Object Type > Attributes
final ASSET_CUSTOMFIELD_ID = 'customfield_12834'
final LAST_MAINTENANCE_ASSETS_ATTRIBUTE_ID = 155
def sourceIssueFields = issue.fields
// Create a date time formatter with the date pattern
final dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
if (!sourceIssueFields.duedate) {
logger.error "No original due date"
return
}
if (!sourceIssueFields[ASSET_CUSTOMFIELD_ID]) {
logger.error "No value in Assets field: $ASSET_CUSTOMFIELD_ID"
return
}
def dueDate = LocalDate.parse(sourceIssueFields.duedate, dateFormatter)
def newDueDate = dueDate.plusMonths(3)
def cloneIssueResponse = post("/rest/api/2/issue")
.header("Content-Type", "application/json")
.body([
// Add any extra fields to clone below
fields: [
summary : sourceIssueFields.summary,
description: sourceIssueFields.description,
project : [
key: sourceIssueFields.project.key
],
issuetype : [
id: sourceIssueFields.issuetype.id
],
duedate: newDueDate.format(dateFormatter).toString(),
// Assets custom field
(ASSET_CUSTOMFIELD_ID): sourceIssueFields[ASSET_CUSTOMFIELD_ID]
]
])
.asObject(Map)
// validate that the clone issue was created correctly
assert cloneIssueResponse.status >= 200 && cloneIssueResponse.status < 300
// Update Assets objects
sourceIssueFields[ASSET_CUSTOMFIELD_ID].each { asset ->
def assetWorkspaceId = asset.workspaceId
def assetObjectId = asset.objectId
def updatedAssetResponse = put("https://api.atlassian.com/jsm/assets/workspace/" + assetWorkspaceId + "/v1/object/" + assetObjectId)
.header("Content-Type", "application/json")
.basicAuth("user@example.com", "<api_token>")
.body([
attributes: [
[
objectTypeAttributeId: LAST_MAINTENANCE_ASSETS_ATTRIBUTE_ID,
objectAttributeValues: [
[
value: dueDate.format(dateFormatter).toString(),
]
]
]
]
])
.asObject(Map)
assert updatedAssetResponse.status >= 200 && updatedAssetResponse.status < 300
}
4. “As each performer needs to have access to the right dates and locations for each show, and this information lives in multiple places, it needs to be consolidated.”
In order to provide everyone with the right itinerary, so they can show up at the right place and at the right time for rehearsals, technical tests etc. The itineraries would need to be built by pulling the dates and locations data, based on which tour each performer is associated with.
Proposed solution: Use Script Listeners to create an itinerary in a custom long text field after a ticket has been created on the customer portal.
Implementation with ScriptRunner for Jira Cloud: Use Script listeners to fill in Custom Fields after a ticket has been created.
You can use the code snippet below to implement a similar solution.
Add a custom long text field after a ticket has been created
//get values from Script Variables
def Apikey = API_TOKEN
def workspaceId = WORKSPACEID
def issueKey = issue.key
logger.info("Issue Key : $issueKey")
//Get the data for the issue
def issueDataFields = issue.fields
logger.info("issue field details")
logger.info(issueDataFields.toString())
//Get the data fields in the instance
def fields = get('/rest/api/3/field')
.asObject(List)
.body as List<Map>
logger.info("defined fields : $fields.toString()")
def person=getIssueField('Employee Name', issueDataFields, fields)
/*
The following script uses three asset tables and pulls all the information together in a comment.
Person table - holds details of performers and crew
Tour Dates table - holds details of the various tour dates including date and venue
Person-Tour date link - allows a multi-multi relationship be built between Person and Tour Dates linking each person to multiple tour venues, and multiple people to a single tour venue.
*/
//Get the person id from the person table
def aql = 'objectType="Person" AND Name="'+person+'"'
logger.info("aql : $aql")
def personIDApi = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
.header('Authorization', "Basic ${Apikey.bytes.encodeBase64()}")
.header('Content-type', "application/json;charset=UTF-8")
.body([
qlQuery: aql
])
.asObject(Map)
logger.info("personIDApi.status :$personIDApi.status")
if (personIDApi.status != 200 || personIDApi.body.values[0] == null) {
//Internal agent error message
errorReport(issueKey,"Step A1: Failed at selecting person ID details from Assets for $person to create itinerary.",true)
//Customer portal error message
errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
//Engineer logfile error message
assert personIDApi.status == 200: """Failure: Assets API returned status code - [$personIDApi.status]
$personIDApi.body"""
return
}
//logger.info("personIDApi.body.values : "+personIDApi.body.values.toString())
def personID= personIDApi.body.values[0].objectKey.toString()
logger.info("Person id: $personID")
//Get the person-tour date links from the Assets API using the AQL to restrict to the person above
aql='objectType= "Person-Tour date link" AND Person = "'+personID+'"'
def tourDates = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
.header('Authorization', "Basic ${Apikey.bytes.encodeBase64()}")
.header('Content-type', "application/json;charset=UTF-8")
.body([
qlQuery: aql
])
.asObject(Map)
logger.info("tourDates.status: "+tourDates.status)
if (tourDates.status != 200 ) {
errorReport(issueKey,"Step A2: Failed at selecting tour dates from Assets for $person to create itinerary.",true)
errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
assert tourDates.status == 200: """Failure: Assets API returned status code - [$tourDates.status]
$tourDates.body"""
//return
}
logger.info("tourDates.body : "+tourDates.body.toString())
def uniqueTourDates = tourDates.body.values.attributes.objectAttributeValues.referencedObject.objectKey.flatten().unique().join(',')
logger.info("""tourDates: $tourDates
- $uniqueTourDates""")
aql='objectType = "Tour Dates" AND Key IN ('+uniqueTourDates+')'
logger.info("aql : $aql")
def venueDetails = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
.header('Authorization', "Basic ${Apikey.bytes.encodeBase64()}")
.header('Content-type', "application/json;charset=UTF-8")
.body([
qlQuery: aql
])
.asObject(Map)
logger.info("venueDetails.status"+venueDetails.status)
if (venueDetails.status != 200) {
errorReport(issueKey,"Step A3: Failed at selecting tour date details from Assets for $person to create itinerary.",true)
errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
assert venueDetails.status == 200: """Failure: Assets API returned status code - [$venueDetails.status]
$venueDetails.body"""
return
}
def tourDateDetails = venueDetails.body.values.attributes.objectAttributeValues.displayValue
logger.info("TourDateDetails - $tourDateDetails")
/*
Details for the document are stored in the tourDateDetails variable and then the following constructs a formatted comment using Atlassian Document Format.
More details on ADF available at https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/ including a playground where you can safely test
your JSON content for format and accuracy.
*/
def fieldContent = """ {
"type": "tableRow",
"content": [
{
"type": "tableHeader",
"attrs": {},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Dates"
}
]
}
]
}, {
"type": "tableHeader",
"attrs": {},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Location Details"
}
]
}
]
}
]
}
"""
def itemDetail = []
tourDateDetails.each
{
logger.info("IT - "+it.toString())
itemDetail = it
logger.info("itemDetail - "+ itemDetail[2] +" | "+ itemDetail[3]+" | "+ itemDetail[7] +" | "+ itemDetail[5] +" | "+ itemDetail[4] +" | ")
fieldContent= """$fieldContent,{
"type": "tableRow",
"content": [
{
"type": "tableCell",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "${itemDetail[2]?.first()} - ${itemDetail[3]?.first()}"
}
]
}
]
},
{
"type": "tableCell",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "${itemDetail[7]?.first()}",
"marks": [
{
"type": "strong"
},
{
"type": "textColor",
"attrs": {
"color": "#bf2600"
}
}
]
},
{
"type": "text",
"text": " in ${itemDetail[5]?.first()} (${itemDetail[4]?.first()})"
}
]
}
]
}
]
}
"""
}
//logger.info("fieldContent : $fieldContent")
def commentBody="""{
"body": {
"version": 1,
"type": "doc",
"content": [
{
"type": "table",
"attrs": {
"isNumberColumnEnabled": false,
"layout": "default"
},
"content":[
$fieldContent
]
}
]
}
}"""
logger.info("commentBody : $commentBody")
def resultComment = post("/rest/api/3/issue/$issueKey/comment")
.header("Content-Type", "application/json")
.body("$commentBody")
.asObject(Map)
if (resultComment.status != 201) {
errorReport(issueKey,"Step D1: Failed at add Itinerary as comment [${resultComment.status}].",true)
errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
assert resultComment.status == 201: """Failure: Comments API returned status code - [$resultComment.status]
$resultComment.body"""
return
}
/**
* Get field content from name from all the fields in the system
* @param issueKey the issue Key that we are working on
* @param message is the message to be added in the comments
* @param internalComment boolean value to determine if internal (true) or external (false) comment
* @return no value
*/
def errorReport(String issueKey, String message, boolean internalComment)
{
def bodyContent = ""
if (internalComment) {
bodyContent = """{
"body": {
"version": 1,
"type": "doc",
"content": [
{
"type": "heading",
"attrs": {
"level": 3
},
"content": [
{
"type": "text",
"text":"$message",
"marks": [
{
"type": "textColor",
"attrs": {
"color": "#bf2600"
}
}
]
}
]
}]
},
"properties": [
{
"key": "sd.public.comment",
"value": {
"internal": true
}
}
]
}"""
}
else
{
bodyContent ="""{
"body": {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text":"$message"
}
]
}]
}
}"""
}
def resultComment = post("/rest/api/3/issue/$issueKey/comment")
.header("Content-Type", "application/json")
.body("$bodyContent")
.asObject(Map)
}
/**
* Get field content from name from all the fields in the system
* @param fieldName the fieldname to be looked up
* @param issueData is the set of field content
* @param fields the definition of fields available in the instance
* @return the content of the requested field
*/
String getIssueField(String fieldName, def issueData, List<Map> fields) {
def issueFieldValue = null
def fieldId = getFieldIdFromName(fieldName, fields)
if(fieldId == null){
logger.info("Unable to find field : "+fieldName)
return null
}
issueData.each { key, dataValue ->
if (key == fieldId) {
logger.info("match key ; $key, fieldID: $fieldId")
issueFieldValue = dataValue?.toString()
}
}
if(issueFieldValue == null || issueFieldValue == "") {
logger.info("Unable to find value for field : "+fieldName)
}
else {
issueFieldValue = issueFieldValue.trim()
}
//logger.warn "CustomField $fieldName: ${issueFieldValue}"
return issueFieldValue
}
/**
* Get field ID from string from all the fields in the system
* @param fieldName the fieldname to be looked up
* @param fields the set of fields available in the instance
* @return the id of the field
*/
def getFieldIdFromName(String fieldName, List<Map> fields) {
def customFieldObject = null
customFieldObject = fields.find { field ->
field.name == fieldName
}
return customFieldObject?.id
}
5. “Local governments need to execute a formal inspection in advance of each show opening.”
These inspections need to be tracked and any Health & Safety actions that are required as a result of the inspection, have to be completed and communicated to the right stakeholders within the organisation to guarantee the show can go on, before hitting opening day dates.
Proposed solution: When Priority is set to Major Incident, create a Slack war room, add a list of relevant stakeholders to it, and then send messages as inspection items are ticked off.
Implementation with ScriptRunner for Jira Cloud: When Priority is set to Major Incident, tag a set of people (watchers or commenters).
You can use the code snippet below, which tags all members of a user group within a comment, to implement a similar solution.
Depending on the notification channel of choice, you can combine the code snippet below with the following scripts in our library for:
Create and populate a Slack channel
final GROUP_NAMES = ["jira-project-leads"]
def issueKey = issue.key
def assignee = issue.fields.assignee
def priorityChange = changelog?.items.find { it['field'] == 'priority' }
if (!priorityChange) {
logger.info("Priority was not updated")
return
}
logger.info("Priority changed from {} to {}", priorityChange.fromString, priorityChange.toString)
if (priorityChange.toString == "Highest") {
def userListFromGroup = []
GROUP_NAMES.each { groupName ->
def groupMemberResp = get("/rest/api/3/group/member")
.queryString('groupname', "${groupName}")
.header("Content-Type", "application/json")
.asObject(Map)
assert groupMemberResp.status == 200
userListFromGroup.addAll(groupMemberResp.body.values)
}
def tags = userListFromGroup.findAll { user -> user.accountId != assignee.accountId }.collect { user ->
[
"type": "mention",
"attrs": [
"id": user.accountId,
"text": "@" + user.displayName,
"accessLevel": ""
]
]
}
def body = [ "body": [
"version": 1,
"type": "doc",
"content": [
[
"type": "paragraph",
"content": tags + [
[
"type": "text",
"text": " This issue requires your attentions."
]
]
]
]
]]
def postCommentResp = post("/rest/api/3/issue/${issueKey}/comment")
.header('Content-Type', 'application/json')
.body(body)
.asObject(Map)
assert postCommentResp.status == 201
}
10 more quick wins in Jira Cloud automation
Looking for more automation use cases you can leverage with ScriptRunner for Jira Cloud?
Not using ScriptRunner yet?
Want to take advantage of these awesome ITSM automations?