Skip to main content
5 ITSM solutions to implement today in Jira Cloud
Share on socials
Browser screen with connected screws
Phill Fox
Phill Fox
19th March, 2024
Jira
Cloud
Cloud icon

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

1def comment = """
2            	Hi, ${issue.fields.reporter.displayName} from ${issue.fields.customfield_12818.value} office,
3
4            	Thank you for creating this ticket in our service desk. You have requested a laptop replacement delivered to following destination:
5
6            	${issue.fields.customfield_12831}
7
8            	Please make sure the address is correct. We will respond to your request shortly.
9
10            	Kindly also note if the ticket remains inactive for a period of 10 days then will automatically be closed.
11        	"""
12
13def addComment = post("/rest/servicedeskapi/request/${issue.key}/comment")
14    	.header('Content-Type', 'application/json')
15    	.body([
16            	body: comment,
17            	// Make comment visible in the customer portal
18            	public: true,
19    	])
20    	.asObject(Map)
21
22assert 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

1final MODEL_ASSETS_CUSTOMFIELD_ID = 'customfield_12832'
2final PHONES_ASSETS_CUSTOMFIELD_ID = 'customfield_12834'
3final ATTRIBUTE_NAME_OF_MODEL_IN_PHONES = 'Model Name'
4
5def issueKey = issue.key
6
7def getModelNameResponse = get('/rest/api/3/issue/' + issueKey)
8    	.header('Content-Type', 'application/json')
9    	.queryString("expand", "${MODEL_ASSETS_CUSTOMFIELD_ID}.cmdb.attributes")
10    	.asObject(Map)
11
12assert getModelNameResponse.status == 200
13
14if (!getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID]) {
15	logger.warn "No value in Assets field: $MODEL_ASSETS_CUSTOMFIELD_ID"
16	return
17}
18
19def workspaceId = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().workspaceId
20def objectSchemaId = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().objectType.objectSchemaId
21def modelName = getModelNameResponse.body.fields[MODEL_ASSETS_CUSTOMFIELD_ID].first().label
22def aql = """
23	"objectSchemaId" = "$objectSchemaId" and "$ATTRIBUTE_NAME_OF_MODEL_IN_PHONES" = "$modelName"
24"""
25
26def aqlQueryResponse = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
27    	.header('Content-Type', 'application/json')
28    	.basicAuth("user@example.com", "<api_token>")
29    	.body([
30            	qlQuery: aql
31    	])
32    	.asObject(Map)
33
34assert aqlQueryResponse.status == 200
35
36def matchedPhoneAssetIds = aqlQueryResponse.body.values*.globalId
37def updatePhonesAssetsFieldResponse = put('/rest/api/3/issue/' + issueKey)
38    	.header('Content-Type', 'application/json')
39    	.body([
40            	fields:[
41                    	(PHONES_ASSETS_CUSTOMFIELD_ID): matchedPhoneAssetIds.collect { id -> [id: id] }
42            	]
43    	]).asString()
44
45assert 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

1import java.time.format.DateTimeFormatter
2import java.time.LocalDate
3
4// To find the Assets Attribute ID. Navigating to Scheme > Object Type > Attributes
5final ASSET_CUSTOMFIELD_ID = 'customfield_12834'
6final LAST_MAINTENANCE_ASSETS_ATTRIBUTE_ID = 155
7
8def sourceIssueFields = issue.fields
9
10// Create a date time formatter with the date pattern
11final dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
12
13if (!sourceIssueFields.duedate) {
14	logger.error "No original due date"
15	return
16}
17
18if (!sourceIssueFields[ASSET_CUSTOMFIELD_ID]) {
19	logger.error "No value in Assets field: $ASSET_CUSTOMFIELD_ID"
20	return
21}
22
23def dueDate = LocalDate.parse(sourceIssueFields.duedate, dateFormatter)
24def newDueDate = dueDate.plusMonths(3)
25
26def cloneIssueResponse = post("/rest/api/2/issue")
27    	.header("Content-Type", "application/json")
28    	.body([
29            	// Add any extra fields to clone below
30            	fields: [
31                    	summary	: sourceIssueFields.summary,
32                    	description: sourceIssueFields.description,
33                    	project	: [
34                            	key: sourceIssueFields.project.key
35                    	],
36                    	issuetype  : [
37                            	id: sourceIssueFields.issuetype.id
38                    	],
39                    	duedate: newDueDate.format(dateFormatter).toString(),
40                    	// Assets custom field
41                    	(ASSET_CUSTOMFIELD_ID): sourceIssueFields[ASSET_CUSTOMFIELD_ID]
42            	]
43    	])
44    	.asObject(Map)
45
46// validate that the clone issue was created correctly
47assert cloneIssueResponse.status >= 200 && cloneIssueResponse.status < 300
48
49// Update Assets objects
50sourceIssueFields[ASSET_CUSTOMFIELD_ID].each { asset ->
51	def assetWorkspaceId = asset.workspaceId
52	def assetObjectId = asset.objectId
53	def updatedAssetResponse = put("https://api.atlassian.com/jsm/assets/workspace/" + assetWorkspaceId + "/v1/object/" + assetObjectId)
54        	.header("Content-Type", "application/json")
55        	.basicAuth("user@example.com", "<api_token>")
56        	.body([
57                	attributes: [
58                        	[
59                                	objectTypeAttributeId: LAST_MAINTENANCE_ASSETS_ATTRIBUTE_ID,
60                                	objectAttributeValues: [
61                                        	[
62                                                	value: dueDate.format(dateFormatter).toString(),
63                                        	]
64                                	]
65                        	]
66                	]
67        	])
68        	.asObject(Map)
69
70	assert updatedAssetResponse.status >= 200 && updatedAssetResponse.status < 300
71}

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

1//get values from Script Variables
2def Apikey = API_TOKEN
3def workspaceId = WORKSPACEID
4
5
6def issueKey = issue.key
7logger.info("Issue Key : $issueKey")
8
9//Get the data for the issue
10def issueDataFields = issue.fields
11logger.info("issue field details")
12logger.info(issueDataFields.toString())
13
14//Get the data fields in the instance
15
16def fields = get('/rest/api/3/field')
17        .asObject(List)
18        .body as List<Map>
19logger.info("defined fields : $fields.toString()")
20    
21 def person=getIssueField('Employee Name', issueDataFields, fields)
22 
23/*
24The following script uses three asset tables and pulls all the information together in a comment. 
25Person table - holds details of performers and crew
26Tour Dates table - holds details of the various tour dates including date and venue
27Person-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.
28
29
30*/
31 //Get the person id from the person table
32 def aql = 'objectType="Person" AND Name="'+person+'"'
33 logger.info("aql : $aql")
34 def personIDApi = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
35    .header('Authorization',  "Basic ${Apikey.bytes.encodeBase64()}")
36    .header('Content-type', "application/json;charset=UTF-8")
37    .body([
38                qlQuery: aql
39    ])
40    .asObject(Map)
41
42logger.info("personIDApi.status :$personIDApi.status")    
43if (personIDApi.status != 200  || personIDApi.body.values[0] == null) {
44    //Internal agent error message
45    errorReport(issueKey,"Step A1: Failed at selecting person ID details from Assets for $person to create itinerary.",true)
46    //Customer portal error message
47    errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
48    //Engineer logfile error message
49    assert personIDApi.status == 200: """Failure: Assets API returned status code - [$personIDApi.status]
50    $personIDApi.body"""
51    return
52
53}
54//logger.info("personIDApi.body.values : "+personIDApi.body.values.toString())
55def personID= personIDApi.body.values[0].objectKey.toString()
56logger.info("Person id: $personID")
57
58 //Get the person-tour date links from the Assets API using the AQL to restrict to the person above
59 aql='objectType= "Person-Tour date link" AND Person = "'+personID+'"'
60 def tourDates = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
61    .header('Authorization',  "Basic ${Apikey.bytes.encodeBase64()}")
62    .header('Content-type', "application/json;charset=UTF-8")
63    .body([
64                qlQuery: aql
65    ])
66    .asObject(Map)
67
68logger.info("tourDates.status: "+tourDates.status)
69if (tourDates.status != 200  ) {
70    errorReport(issueKey,"Step A2: Failed at selecting tour dates from Assets for $person to create itinerary.",true)
71    errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
72    assert tourDates.status == 200: """Failure: Assets API returned status code - [$tourDates.status]
73        $tourDates.body"""
74    //return
75
76}
77logger.info("tourDates.body : "+tourDates.body.toString())
78def uniqueTourDates = tourDates.body.values.attributes.objectAttributeValues.referencedObject.objectKey.flatten().unique().join(',')
79logger.info("""tourDates: $tourDates 
80- $uniqueTourDates""")
81
82 aql='objectType = "Tour Dates" AND Key IN ('+uniqueTourDates+')'
83 logger.info("aql : $aql")
84 def venueDetails = post("https://api.atlassian.com/jsm/assets/workspace/$workspaceId/v1/object/aql")
85    .header('Authorization',  "Basic ${Apikey.bytes.encodeBase64()}")
86    .header('Content-type', "application/json;charset=UTF-8")
87    .body([
88                qlQuery: aql
89    ])
90    .asObject(Map)
91
92logger.info("venueDetails.status"+venueDetails.status)
93if (venueDetails.status != 200) {
94    errorReport(issueKey,"Step A3: Failed at selecting tour date details from Assets for $person to create itinerary.",true)
95    errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
96    assert venueDetails.status == 200: """Failure: Assets API returned status code - [$venueDetails.status]
97        $venueDetails.body"""
98    return
99
100}
101def tourDateDetails  = venueDetails.body.values.attributes.objectAttributeValues.displayValue
102logger.info("TourDateDetails - $tourDateDetails")
103  /*
104Details for the document are stored in the tourDateDetails variable and then the following constructs a formatted comment using Atlassian Document Format.
105More details on ADF available at https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/ including a playground where you can safely test
106your JSON content for format and accuracy.
107  
108      */
109def fieldContent = """ {
110            "type": "tableRow",
111            "content": [
112              {
113                "type": "tableHeader",
114                "attrs": {},
115                "content": [
116                  {
117                    "type": "paragraph",
118                    "content": [
119                      {
120                        "type": "text",
121                        "text": "Dates"
122                      }
123                    ]
124                  }
125                ]
126              }, {
127                "type": "tableHeader",
128                "attrs": {},
129                "content": [
130                  {
131                    "type": "paragraph",
132                    "content": [
133                      {
134                        "type": "text",
135                        "text": "Location Details"
136                      }
137                    ]
138                  }
139                ]
140              }
141              ]
142}
143              """
144def itemDetail = []
145 tourDateDetails.each
146{
147    logger.info("IT - "+it.toString())
148    itemDetail = it
149    logger.info("itemDetail - "+ itemDetail[2] +" | "+ itemDetail[3]+" | "+ itemDetail[7] +" | "+ itemDetail[5] +" | "+ itemDetail[4]  +" | ")
150    fieldContent= """$fieldContent,{
151            "type": "tableRow",
152            "content": [
153              {
154                "type": "tableCell",
155                "content": [
156                  {
157                    "type": "paragraph",
158                    "content": [
159                      {
160                        "type": "text",
161                        "text": "${itemDetail[2]?.first()} - ${itemDetail[3]?.first()}"  
162                      }
163                    ]
164                  }
165                ]
166              },
167              {
168                "type": "tableCell",
169                "content": [
170                  {
171                    "type": "paragraph",
172                    "content": [
173                    {
174                        "type": "text",
175                        "text": "${itemDetail[7]?.first()}",
176						"marks": [
177							{
178								"type": "strong"
179							},
180							{
181								"type": "textColor",
182								"attrs": {
183									"color": "#bf2600"
184								}
185							}
186							]
187					},
188                    {
189                        "type": "text",
190                        "text": " in ${itemDetail[5]?.first()} (${itemDetail[4]?.first()})"
191                      }
192                      ]
193                  }
194                ]
195              }
196            ]
197          }
198           
199            """
200}
201
202//logger.info("fieldContent : $fieldContent")
203
204def commentBody="""{
205  "body": {
206    "version": 1,
207    "type": "doc",
208    "content": [
209      {
210        "type": "table",
211        "attrs": {
212          "isNumberColumnEnabled": false,
213          "layout": "default"
214        },
215        "content":[
216        $fieldContent
217        ]
218      }
219    ]
220  }
221}"""
222logger.info("commentBody : $commentBody")
223def resultComment = post("/rest/api/3/issue/$issueKey/comment")
224        .header("Content-Type", "application/json")
225        .body("$commentBody")
226        .asObject(Map)       
227 
228 if (resultComment.status != 201) {
229    errorReport(issueKey,"Step D1: Failed at add Itinerary as comment [${resultComment.status}].",true)
230    errorReport(issueKey,"Unable to automatically produce your itinerary. Please contact your tour director directly.",false)
231     assert resultComment.status == 201: """Failure: Comments API returned status code - [$resultComment.status]
232        $resultComment.body"""
233    return
234 }
235 
236  /**
237* Get field content from name from all the fields in the system
238* @param issueKey the issue Key that we are working on
239* @param message is the message to be added in the comments
240* @param internalComment boolean value to determine if internal (true) or external (false) comment
241* @return no value
242*/
243       
244 def errorReport(String issueKey, String message, boolean internalComment)   
245 {
246     def bodyContent = ""
247
248     if (internalComment) {
249        bodyContent = """{
250            "body": {
251                "version": 1,
252                "type": "doc",
253				"content": [
254					{
255						"type": "heading",
256						"attrs": {
257							"level": 3
258						    },
259						"content": [
260                             {
261                             "type": "text",
262                             "text":"$message",
263                             "marks": [
264									{
265										"type": "textColor",
266										"attrs": {
267											"color": "#bf2600"
268										}
269									}
270								]
271                             }
272                             ]
273					}]
274                },
275  "properties": [
276       {
277         "key": "sd.public.comment",
278         "value": {
279           "internal": true
280         }
281       }
282     ]
283            }"""
284     }
285     else
286     {
287         bodyContent ="""{
288            "body": {
289                "version": 1,
290                "type": "doc",
291				"content": [
292					{
293						"type": "paragraph",
294						"content": [
295                             {
296                             "type": "text",
297                             "text":"$message"
298                             }
299                             ]
300					}]
301                }
302            }"""
303 }
304     def resultComment = post("/rest/api/3/issue/$issueKey/comment")
305        .header("Content-Type", "application/json")
306        .body("$bodyContent")
307        .asObject(Map)  
308     
309 }
310 
311 /**
312* Get field content from name from all the fields in the system
313* @param fieldName the fieldname to be looked up
314* @param issueData is the set of field content
315* @param fields the definition of fields available in the instance
316* @return the content of the requested field
317*/
318 String getIssueField(String fieldName, def issueData, List<Map> fields) {
319    def issueFieldValue = null
320    
321    def fieldId = getFieldIdFromName(fieldName, fields)
322	
323	if(fieldId == null){
324		logger.info("Unable to find field : "+fieldName)
325		
326		return null
327	}
328    
329    issueData.each { key, dataValue ->
330        if (key == fieldId) {
331            logger.info("match key ; $key, fieldID: $fieldId")
332            issueFieldValue = dataValue?.toString()
333            }
334        }
335    
336    if(issueFieldValue == null || issueFieldValue == "") {
337		logger.info("Unable to find value for field : "+fieldName)
338	}
339	else {
340            issueFieldValue = issueFieldValue.trim()
341	}
342
343    //logger.warn "CustomField $fieldName: ${issueFieldValue}"
344    return issueFieldValue
345}
346/**
347* Get field ID from string from all the fields in the system
348* @param fieldName the fieldname to be looked up
349* @param fields the set of fields available in the instance
350* @return the id of the field
351*/
352def getFieldIdFromName(String fieldName, List<Map> fields) {
353
354	def customFieldObject = null
355
356    customFieldObject = fields.find { field ->
357        field.name == fieldName
358    }
359    
360    return customFieldObject?.id
361}

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

1final GROUP_NAMES = ["jira-project-leads"]
2
3def issueKey = issue.key
4def assignee = issue.fields.assignee
5
6def priorityChange = changelog?.items.find { it['field'] == 'priority' }
7
8if (!priorityChange) {
9	logger.info("Priority was not updated")
10	return
11}
12logger.info("Priority changed from {} to {}", priorityChange.fromString, priorityChange.toString)
13
14if (priorityChange.toString == "Highest") {
15	def userListFromGroup = []
16	GROUP_NAMES.each { groupName ->
17    	def groupMemberResp = get("/rest/api/3/group/member")
18            	.queryString('groupname', "${groupName}")
19            	.header("Content-Type", "application/json")
20            	.asObject(Map)
21
22    	assert groupMemberResp.status == 200
23    	userListFromGroup.addAll(groupMemberResp.body.values)
24	}
25
26	def tags = userListFromGroup.findAll { user -> user.accountId != assignee.accountId }.collect { user ->
27    	[
28            	"type": "mention",
29            	"attrs": [
30                    	"id": user.accountId,
31                    	"text": "@" + user.displayName,
32                    	"accessLevel": ""
33            	]
34    	]
35	}
36
37	def body = [ "body": [
38        	"version": 1,
39        	"type": "doc",
40        	"content": [
41                	[
42                        	"type": "paragraph",
43                        	"content": tags + [
44                                	[
45                                        	"type": "text",
46                                        	"text": " This issue requires your attentions."
47                                	]
48                        	]
49                	]
50        	]
51	]]
52
53	def postCommentResp = post("/rest/api/3/issue/${issueKey}/comment")
54        	.header('Content-Type', 'application/json')
55        	.body(body)
56        	.asObject(Map)
57
58	assert postCommentResp.status == 201
59}

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?