Getting Started

JIRA Extension Points

If you are familiar with extensions to workflows from JIRA Server then some of the concepts here are a little different. Post Functions are implemented as asynchronous webhooks. This means that:

Also missing from the cloud edition are Scripted Conditions and Scripted Validators. JIRA Cloud does not provide extension points to write equivalent extensions to ScriptRunner on JIRA Server

Post Functions

ScriptRunner has several built in Post Functions. Each function contains a condition script that can be used to veto the rest of the script by returning false. Most functions also have additional code that can execute to modify the final action of the function.

Conditions

Conditions are able to provide some additional logic to restrict execution beyond what is achievable using workflow schemes. For example issue.fields.summary ==~ /^(?i)foo.*/ will ensure that the function only executes if the summary starts with "foo" (case insensitive). Other examples are:

  • Never Execute : false

  • Match only a specific issue type : ((Map) issue.fields.issuetype)?.name == 'Task'

  • Ensure that fields are present, in this case assignee must be set : issue.fields.assignee != null

Additional Code

Each Post Function that takes additional code will have a specific script context inserted into the additional code script. The area immediately above the code editor will display the parameters already available in your code and link to more documentation.

Script Context

The Script Context is a set of parameters/code variables that are automatically injected into your script to provide contextual data for the Post Function. They are also available for the Condition and Additional Code fields and are displayed immediately above the code editor.

The Script Context for Additional Code and Conditions contain:

  • baseUrl - Base url to make API requests against. This is the URL used for relative request paths e.g. if you make a request to /rest/api/2/issue we use the baseUrl to create a full request path.

  • logger - Logger to use for debugging purposes

  • issue - Transitioned issue details available as Map, for more details see Get Issue REST API reference

  • transitionInput - The transition for the issue as a Map, for more details see Atlassian Connect Workflow Post Function Documentation

Run As User

Post Functions can make requests back to JIRA using either the ScriptRunner Add-on user or the user that performed the transition to cause the Post Function to run. These users are referred to as the 'ScriptRunner Add-On User' and the 'Initiating User'. The advantage of using the Initiating User is that any action that occur as a result of the function will be registered as being performed by them. For example if an issue is commented on, the comment will come from them rather than the obscure ScriptRunner Add-on User. Their permissions will also be applied, which is the key reason to choose between users. Typically the Add-on user has project admin permissions, however it is possible to restrict that and it might be the case that the Initiating User has greater permissions than the Add-on User.

If your scripts are triggered by events invoked by another addon, they will be run as a ScriptRunner user even if you set it up to be run as current user, because it’s not possible to impersonate other addon users. We assume that a user is an addon user if its name starts with a prefix 'addon_'.

Provided Functions

Assign Issue

Assign issue takes the issue and assign it to the last assignee with a specified role or from user group. You must specify either project role or user group. If both are defined, project role will be used and user group will be skipped. Example usage scenario:

  • Dev works on an issue (is in the developer role)

  • Marks the issue as ready to test (issue becomes unassigned)

  • Tester picks up the issue (is automatically assigned)

  • Tester rejects the issue passing it back to dev. The last user in the developer role is assigned

  • The dev fixes the issue (or explains that a bug is a feature) and sends it back to test. The issue is assigned to the tester that tested it (last user in the tester role)

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the issue will be assigned. See Conditions for examples.

  • Project Role - User with this role will be selected.

  • User Group - If you do not specify project role, user from this group will be selected.

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

See the available Script Context for Condition and Code fields in the Script Context section.

Clone Issue

Clone Issue takes the issue and creates a clone of it in. You can specify the target project, issue type, the kind of link and its direction that is created between the source issue and the clone. It is possible to totally override issueInput with a new structure by setting issueInput from additional code however this is not recommended.

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the issue will be cloned. See Conditions for examples.

  • Issue Type - Issue type of target issue. Leave blank for same issue type as source issue

  • Target Project - Which project to clone to. Leave blank for current project

  • Link Name - Link to create between the issue and clone. Leave blank for no link

  • Additional Code - Modify the issueInput structure before it is used as the POST body to /rest/api/2/issue to create the issue

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

See the available Script Context for Condition and Code fields in the Script Context section.

Create Subtask

This function will create a subtask for the issue that is being transitioned. It will not do anything for issues that are subtasks. It is possible to specify the subtask type and title along with executing additional code. Additional code will have issueInput bound as the structure that will be used in the post to /rest/api/2/issue to create the subtask. Overriding issueInput is possible by setting issueInput as part of the script.

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the subtask will be created. See Conditions for examples.

  • Issue Type - The Subtask issue type

  • Subtask Summary - The Subtask summary

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

  • Additional Code - Modify the subtask structure before it is used as the POST body to /rest/api/2/issue to create the subtask

See the available Script Context for Condition and Code fields in the Script Context section.

Fast-Track Transition Issue

Fast-track Transition Issue takes the issue and performs a transition on it immediately. An optional comment can be supplied. Transitions are specified by name, and must be valid for the issue that is to be transitioned. Due to limitations in the JIRA REST API it is not possible to validate transition names until execution.

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the issue will be transited. See Conditions for examples.

  • Transition Comment - Comment to add during the transition. It will be added only if the transition allows adding a comment

  • Transition ID - ID of the transition to perform on the issue. You must specify either transition ID or name. ScriptRunner will use the transition name if no ID is provided. In some cases it’s not possible to find transition ID by name e.g. if the 'Hide From User' Condition is configured on the transition, therefore in general you should use ID rather than name. You can find the transition ID on the edit/view workflow page, the number in parenthesis is a transition ID e.g. Start Progress (4)

  • Transition Name - Name of the transition to perform on the issue. This must exist and be valid for the issue to be fast tracked

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

  • Additional Code - Modify the transition structure before it is used as the PUT body to /rest/api/2/issue/<issue.id>/transition to modify the issue when transitioned

See the available Script Context for Condition and Code fields in the Script Context section.

Run Script

Run arbitrary code on transition

Fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining if the condition for which the script will run. See Conditions for examples.

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

  • Code - The code to run on transition

See the available Script Context for Condition and Code fields in the Script Context section.

Add/Remove from/to active sprint

This function has two complementary parts - either adding an issue to an active sprint, or removing it from its current sprint.

For example, on the Start Progress transition you might apply this function so it’s automatically added to the current sprint. This is not the scrum way, where the entire sprint should be planned in advance, and if you finish early you should just twiddle your thumbs (not really true). However, sometimes you finish all the work in a sprint, and it’s a pain to then have to manually add everything you’ve worked on to the current sprint. In the function configuration you provide a board name, the function will pick the first active sprint from that board.

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the issue will be transited. See Conditions for examples.

  • Action - Whether to add or remove to/from sprint. Choose one of the options

  • Board Name - If you are adding to a sprint, you need to select a board

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

See the available Script Context for Condition and Code fields in the Script Context section.

Send Notification

The Send Notification function allows the author to programmatically generate an email notification to be sent to a number of users and/or groups including Watchers, Voters, Reporter and Assignee.

Note: The notify API which this script uses contains a validation rule which prevents users from notifying themselves. This means that the execution will fail if the user being notified is the same user who executed the script.

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining if the condition for which the notification will happen.

  • Recipients of the notification

    • Watchers, Voters, Reporter, Assignee - select who to send the notification to

    • Users - Comma separated list of users to send the notification to

    • Groups - Comma separated list of groups to send the notification to

  • Subject - The subject of the notification, required

  • Message - Groovy code that returns the message to send as notification body, see examples for usage

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

  • Additional Code - Modify the notification structure before it is used as the POST body to /rest/api/2/issue/<issue id>/notify to send the notification

Here’s an example of constructing an email from the issue details

Use this in the Condition field to specify that the issue must have an assignee

issue.fields.assignee != null

Use this in the Message field to retrieve the value for the 'TextFieldB' custom field and construct the notification body

def fields = get('/rest/api/2/field')
        .asObject(List)
        .body as List<Map>

def customFieldId = fields.find { it.name == 'TextFieldB' }.id as String  (1)
def customFieldValue = (issue.fields[customFieldId] as Map)?.value

"""Dear ${issue.fields.assignee?.displayName},

The ${issue.fields.issuetype.name} ${issue.key} with priority ${issue.fields.priority?.name} has been assigned to you.

Description: ${issue.fields.description}

Custom field value: ${customFieldValue}

Regards,
${issue.fields.reporter?.displayName}"""
1 Retrieve the custom field ID for the 'TextFieldB' custom field

Transition Parent Issue

A function for subtasks. This will do nothing for issues that are not subtasks. The specified transition will be performed on the parent of the subtask. As with Fast-Track Transition Issue, the transition name is provided and not validated.

See Fast-Track Transition Issue for fields details that will apply to the parent issue.

Modify Issue

Modify Issue gives the script author the ability to update the issue or perform any action, by cancelling the update. The update may be cancelled by setting issueInput to null. This maybe useful to use a Post Function in a similar way to a Script Listener.

The fields

  • Description - Logical name of the Post Function

  • Condition - Groovy code that returns boolean value determining the condition for which the issue will be modified. See Conditions for examples.

  • Run As - Function can be executed either as the user that caused the transition or the ScriptRunner Add-On User

  • Additional Code - Modify the issueInput structure before it is used as the PUT body to /rest/api/2/issue/<issue.id> to modify the issue. Running as the Add-on user adds overrideScreenSecurity=true as a query parameter to allow editing fields that are not on the screen.

See the available Script Context for Condition and Code fields in the Script Context section.

Example Custom Scripts

Calculated Custom Field

This example is similar to the calculated field example for Script Listeners except in this case the calculation is only performed when the issue on transitions specified in the workflow.

// get custom fields
def customFields = get("/rest/api/2/field")
        .asObject(List)
        .body
        .findAll { (it as Map).custom } as List<Map>

def input1CfId = customFields.find { it.name == 'Custom Field 1' }?.id
def input2CfId = customFields.find { it.name == 'Custom Field 2' }?.id
def outputCfId = customFields.find { it.name == 'Output Custom Field' }?.id

def input1 = issue.fields[input1CfId] as Integer (1)
def input2 = issue.fields[input2CfId] as Integer

if (input1 == null || input2 == null) { (2)
    logger.info("Calculation using ${input1} and ${input2} was not possible")
    return
}

def output = input1 + input2

if (output == (issue.fields[outputCfId] as Integer)) { (3)
    logger.info("already been updated")
    return
}

put("/rest/api/2/issue/${issue.key}") (4)
    // .queryString("overrideScreenSecurity", Boolean.TRUE) (5)
    .header("Content-Type", "application/json")
    .body([
        fields:[
                (outputCfId): output
        ]
    ])
    .asString()
1 Grab the values of the custom fields from the event
2 Sanity check for null values - the issue may not have a value for any input
3 Here we check to see if the output field already has the calculated value, if it does do nothing. This is important because an IssueUpdated event will fire for the update we are about to perform
4 Update the value of the custom field
5 If using the Add-on user to run the script, it is possible to set the overrideScreenSecurity property to modify fields that are not on the current screen

Post to HipChat

This is an example of a very simple HipChat post, that updates a room with a notification that an issue has been updated. Extending the example would give the author the ability to generate a more complex message.

def apiToken = "YOUR_SECRET_TOKEN" (1)

Unirest.clearDefaultHeaders() (2)
post("https://api.hipchat.com/v2/room/test room/notification")
        .header("Content-Type", "application/json")
        .queryString('auth_token', apiToken)
        .body([
                message       : "<p>${issue.key} Updated</p>",
                notify        : false,
                message_format: "html"
        ])
        .asString() (3)
issueInput = null (4)
1 Generate a HipChat API token and use it here
2 Clear the Unirest default headers to remove the JWT token header
3 Make the POST request to hipchat with the auth_token
4 Prevent the script from updating the issue

Create Subtask

Here is the code we run under the hood in the provided function, modified slightly so you can run it in the script console:

// Here we specify and retrieve the details of the parent issue
// If you copied this code into a Post Function or an issue-related Script Listener you could remove
// the first 5 lines of code as an issue variable would already be available to your script
def parentKey = 'DEMO-1'
def issueResp = get("/rest/api/2/issue/${parentKey}")
        .asObject(Map)
assert issueResp.status == 200
def issue = issueResp.body as Map

// We retrieve all issue types
def typeResp = get('/rest/api/2/issuetype')
        .asObject(List)
assert typeResp.status == 200
def issueTypes = typeResp.body as List<Map>

// Here we set the basic subtask issue details
def summary = "Subtask summary"
def issueType = "Sub-task"
def issueTypeId = issueTypes.find { it.subtask && it.name == issueType }?.id

assert issueTypeId : "No subtasks issue type found called '${issueType}'"

def createDoc = [
        fields: [
                project: (issue.fields as Map).project,
                issuetype: [
                        id: issueTypeId
                ],
                parent: [
                        id: issue.id
                ],
                summary: summary
        ]
]

// Now we create the subtask
def resp = post("/rest/api/2/issue")
        .header("Content-Type", "application/json")
        .body(createDoc)
        .asObject(Map)
def subtask = resp.body
assert resp.status >= 200 && resp.status < 300 && subtask && subtask.key != null

subtask

Here is an example of automatically linking the issue being transitioned to another issue:

def issueId = issue.id

def link = post('/rest/api/2/issueLink')
        .header('Content-Type', 'application/json')
        .body([
        type: [ name: "Blocks" ],
        outwardIssue: [ id: issueId ],  // This is the issue that the link 'starts' at
        inwardIssue: [ key: 'EX-1' ]  // You'll need to specify an issue ID or key here
])
        .asString()
assert link.status == 201