Custom Post Functions

For a tutorial on workflow post functions, see the Post Functions Tutorial page.

Most of the built-in workflow functions can be customized with Condition code or Additional actions code, so if you can use a built-in script, you should.

Post functions

Post functions execute after the transition has been validated. Here is the place where you can pass on a message to a downstream system, send custom notifications or modify the issue etc.

Post function order

The order of the post functions is important. For example, a post function that changes the description of the issue - issue.setDescription("A new description") - should be placed before the Update change history for an issue and store the issue in the database step. Fast track transition an issue should be placed after the Fire Event step.

Where to put your scripts?

You can write your script inline, or if you prefer to use files, you can use the File tab and provide the absolute path or a relative path to a script stored in one of your script roots. Relative paths are more portable and make switching servers easier.

Script binding

For each type of workflow function, the plugin provides several binding variables. For example, there is a log variable to help debug your scripts, an issue variable to access the current issue that the function applies to, and a transientVars map variable to access variables unique to the transition. 

To view the script binding variables, click the small blue circle with a ? near the script input area. 

def issueKey = issue.key def actionId = transientVars['actionId'] log.debug("Issue key is ${issueKey}, and the action id is ${actionId}")

If you use Intellij IDEA, you can add a dynamic property for these variables of the correct type. Alternatively, you can just redeclare it with type information:

import com.atlassian.jira.issue.Issue; Issue issue = issue

Examples

Append generated comment

Automatically append some generated comment on a transition.

In this case we add a comment if a user resolved an issue which had unresolved subtasks:

import com.atlassian.jira.component.ComponentAccessor

def commentManager = ComponentAccessor.getCommentManager()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

if (issue.subTaskObjects.any { !it.resolution }) {
    commentManager.create(
        issue,
        user,
        "I resolved this issue even though there were unresolved sub-tasks... slapped wrists",
        false) // <1>
}

Auto add reviewers based on request type

Case Study:

When an issue is created, users have to manually select the reviewer of their task. The admin wants to set it automatically depending on the request.

This makes sense if, for example, a purchase over 100 dollars needs to be cleared by one department and a travel request needs to be cleared by your manager.

Steps:
  1. Select the onCreate transition of your workflow.
    Remember this can only be accessed through the diagram, and not the text editor.
  2. Select Add post function > Custom script post-function.

    It is very important that this is the FIRST function if set on the onCreate transition, else it will not update your value.

  3. Add the following code:

    Please note that in this case study, we want to add reviewers for the two basic request types that require reviewers: "Purchase Over $100", and "Travel Request".

    import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
    import com.atlassian.servicedesk.api.requesttype.RequestTypeService
    import com.onresolve.scriptrunner.runner.customisers.WithPlugin
    import com.opensymphony.workflow.WorkflowContext
    
    @WithPlugin("com.atlassian.servicedesk")
    
    /*This is a map of request-Type -> Request Reviewers*/
    def userMap = [
        "Purchase over \$100": ["admin"], // <1>
        "Travel request"     : ["anuser", 'user2', 'user3'] // <2>
    ]
    
    def currentUserId = ((WorkflowContext) transientVars.get("context")).getCaller()
    def currentUser = ComponentAccessor.getUserManager().getUserByKey(currentUserId)
    
    /*----------------------------------------------------------------*/
    
    if (issue.issueType.name == "Service Request with Approvals") {
    
        def requestTypeService = ComponentAccessor.getOSGiComponentInstanceOfType(RequestTypeService)
    
        def sourceIssueRequestTypeQuery = requestTypeService
            .newQueryBuilder()
            .issue(issue.id)
            .requestOverrideSecurity(true)
            .build()
    
        def changeHolder = new DefaultIssueChangeHolder()
    
        def customFieldManager = ComponentAccessor.getCustomFieldManager()
        def requestTypeEither = ComponentAccessor.getOSGiComponentInstanceOfType(RequestTypeService).getRequestTypes(currentUser, sourceIssueRequestTypeQuery)
    
        if (requestTypeEither.isLeft()) {
            log.error "${requestTypeEither.left().get()}"
            return
        }
    
        def requestType = requestTypeEither.right.results[0]
        def cfChange = customFieldManager.getCustomFieldObjectByName("Approvers")
        def userManager = ComponentAccessor.getUserManager()
    
        /*This is done, so that if a user adds a reviewer, it doesn't get replaced, and instead it's added to the list*/
        def list = issue.getCustomFieldValue(cfChange) ?: [] // <3>
    
        if (userMap.keySet().contains(requestType.name)) {
            def users = userMap[requestType.name]
            def toAdd = users.findResults {
                userManager.getUserByKey(it) ?: log.warn("User with key ${it} does not exist")
            }
    
            list.addAll(toAdd)
            issue.setCustomFieldValue(cfChange, list)
        }
    }

Auto close sub-tasks

Script to Resolve all currently Open sub-tasks.

import com.atlassian.jira.component.ComponentAccessor

def issueService = ComponentAccessor.getIssueService()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def subTasks = issue.getSubTaskObjects()
subTasks.each {
    if (it.statusObject.name == "Open") {
        def issueInputParameters = issueService.newIssueInputParameters()
        issueInputParameters.with {
            setResolutionId("1") // resolution of "Fixed"
            setComment("*Resolving* as a result of the *Resolve* action being applied to the parent.")
            setSkipScreenCheck(true)
        }

        // validate and transition subtask
        def validationResult = issueService.validateTransition(user, it.id, 5, issueInputParameters)
        if (validationResult.isValid()) {
            def issueResult = issueService.transition(user, validationResult)
            if (!issueResult.isValid()) {
                log.warn("Failed to transition subtask ${it.key}, errors: ${issueResult.errorCollection}")
            }
        } else {
            log.warn("Could not transition subtask ${it.key}, errors: ${validationResult.errorCollection}")
        }
    }
}

Get workflow steps

Get the current action name

In a workflow function, to get the ID of the action (which is normally what you want rather than the name), use:

transientVars["actionId"]

To get the action name you can use:

import com.atlassian.jira.component.ComponentAccessor

def workflow = ComponentAccessor.getWorkflowManager().getWorkflow(issue)
def wfd = workflow.getDescriptor()
def actionName = wfd.getAction(transientVars["actionId"] as int).getName()
log.debug("Current action name: $actionName")

Get the previous and destination steps

Getting the current and destination steps depends on whether your function is placed above or below the functions to update the issue in the database and reindex.

The following code:

myIssue.statusObject.name

will return the current status - so if your script function is before the built-in functions to update the issue it will return the "previous" status, if it is after it will return the destination status.

To get the destination step use:

import com.atlassian.jira.component.ComponentAccessor
import com.opensymphony.workflow.spi.Step

def step = transientVars["createdStep"] as Step
def stepId = step.getStepId()
def status = ComponentAccessor.getConstantsManager().getStatus(stepId as String)

log.debug(status.name)

Set issue fields

You can see some issue fields as part of a post-function, for instance components, versions, custom and system fields.

The following example covers these areas.

You can only use this method to update issue fields if your script post-function comes before the standard function Update change history for an issue and store the issue in the database. If you need your function to run after that, you have to use a more complex method.

import com.atlassian.jira.component.ComponentAccessor

import java.sql.Timestamp

def versionManager = ComponentAccessor.getVersionManager()
def projectComponentManager = ComponentAccessor.getProjectComponentManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def userManager = ComponentAccessor.getUserManager()

def project = issue.getProjectObject()

def version = versionManager.getVersion(project.getId(), "1.1")
def component = projectComponentManager.findByComponentName(project.getId(), "MyComponent")

if (version) {
    issue.setFixVersions([version])
}

if (component) {
    issue.setComponent([component])
}

// a text field
def textCf = customFieldManager.getCustomFieldObjectByName("TextFieldA") // <1>
issue.setCustomFieldValue(textCf, "Some text value")

// a date time field - add 7 days to current datetime
def dateCf = customFieldManager.getCustomFieldObjectByName("First Date") // Date time fields require a Timestamp
issue.setCustomFieldValue(dateCf, new Timestamp((new Date() + 7).time))

// a user custom field
def userCf = customFieldManager.getCustomFieldObjectByName("UserPicker")
issue.setCustomFieldValue(userCf, userManager.getUserByName("admin")) // User CFs require an ApplicationUser

// system fields
issue.setDescription("A generated description")
issue.setDueDate(new Timestamp((new Date() + 1).time)) // set due date to tomorrow

Setting Checkbox Fields

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.customfields.manager.OptionsManager

def optionsManager = ComponentAccessor.getComponent(OptionsManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()

def cf = customFieldManager.getCustomFieldObjectByName("Checkboxes") // Checkboxes is the NAME of my custom field
def fieldConfig = cf.getRelevantConfig(issue)
def option = optionsManager.getOptions(fieldConfig).getOptionForValue("Yes", null)
issue.setCustomFieldValue(cf, [option]) // <1>

Line 10: The custom field value is a Collection of Option objects

Setting Radio Button Fields

Identical to the above example, except the custom field contains only a single Option, so do not create a List, example:

groovy
issue.setCustomFieldValue(cf, option)
On this page