Custom Post Functions
Use the Custom script post-function to run a custom script after the transition has been validated. This is where you can pass on a message to a downstream system, send custom notifications, modify the issue, and more.
If you need help writing your script, check out the Scripting tips page.
Recommendations for writing a custom post function
Post functions are executed after a transition has been completed. They are meant for performing actions on the transitioned issue or related tasks that don't require user interaction. For example, you can use a custom post function to update the transitioned issue, create or update related issues, and send notifications or emails. However, there are limitations to custom post functions, some of which can impact performance. We therefore recommend the following:
- Keep it simple: Avoid complex operations that could delay the transition or require user input. If complex searches or operations are required, use a scheduled job or a listener instead of a post function.
- Optimize searches: If a search is necessary, narrow down the search criteria as much as possible to minimize performance impact.
- Avoid loops with
Issues.search(): Instead of usingIssues.search()in loops, perform the search once and store the results for further processing. - Use efficient retrieval methods: Consider alternative methods to retrieve related issues, such as direct ID lookups or JQL queries with specific issue keys.
- Use bulk operations: For tasks involving multiple issues, use bulk operations or batching to improve performance (refer to our Built-In Scripts for examples).
- Test thoroughly: Always test scripts in a non-production environment to ensure they behave as expected and don't negatively impact system performance.
- Use logging: Implement appropriate logging to aid in debugging and understanding script behavior, but be mindful of excessive logging that could impact performance.
- Monitor and optimize: Regularly review and optimize your custom post functions to ensure they continue to perform efficiently as your Jira instance grows.
- Avoid calling
issue.transition()on the current issue inside a post-function: Doing so may cause the current status to not update correctly. See our Transition issues page for more details.
Post function order
The order of post functions in a Jira workflow transition is important and should follow the logical sequence of operations that need to occur during a transition. Any updates to the issue should be made before the issue is saved and reindexed, and any additional transitions should happen after the main transition events have been fired and processed.
Update description example
If you have a post function that changes the description of the issue, for example issue.setDescription("A new description"), it should be placed before the Update change history for an issue and store the issue in the database step. If you place the function after the issue has been saved, the changes won't be made in the database or recorded in the change history. This means that the change never occurred from the perspective of other users and the system.
Fast-track transition an issue example
Fast-track transition an issue should be placed after the Fire Event step. If you fast-track an issue to another status before firing the event, the notifications might not reflect the intermediate transition. By placing the fast-track transition after the event firing, you ensure that the issue's history and notifications accurately reflect the sequence of transitions that the issue went through.
Use this post function
You can now test to see if this post function works.
Examples
Each of the examples below is based on specific workflows and project types. Make sure you adjust the examples appropriately to suit your workflow/s.
Add a generated comment
You can automatically add a generated comment on a transition. In this example, we add a comment if a user resolved an issue that has unresolved subtasks:
if (issue.subTaskObjects.any { !it.resolution }) {
issue.addComment('I resolved this issue even though there were unresolved sub-tasks...') {
setDispatchEvent(false)
}
}Auto-add reviewers based on request type
You can automatically add reviewers to tasks depending on the request type. In this example, a purchase over $100 needs to be cleared by one department, and a travel request needs to be cleared by your manager:
/*This is a map of request-Type -> Request Reviewers*/
def requestTypeToApprovers = [
"Purchase over \$100": ["financeTeamUser", "lineManagerUser"],
"Travel request" : ["travelTeamUser", "lineManagerUser"]
]
if (issue.issueType.name == "Service Request with Approvals") {
def requiredApproverNames = requestTypeToApprovers[issue.requestType?.name]
if (requiredApproverNames) {
issue.set {
setCustomFieldValue('Approvers') {
requiredApproverNames.each {
add(it)
}
}
}
}
}Auto close sub-tasks
You automatically resolve sub-tasks. In this example, we Resolve all currently open sub-tasks when the parent task is transitioned to Resolved.
def subTasks = issue.subTaskObjects
subTasks.each { subTask ->
if (subTask.status.name == "Open") {
subTask.transition('Resolve Issue') {
setResolution('Done')
}
}
}Set issue fields
You can automatically set some issue fields, such as components, versions, custom, and system fields. The following example covers these areas:
import java.time.LocalDate
issue.set {
setFixVersions('1.1')
setComponents('MyComponent')
// a text field
setCustomFieldValue('TextFieldA', "Some text value")
// a date time field - add 7 days to current datetime
setCustomFieldValue('Date Picker') {
set(get().plusDays(7))
}
// checkboxes, radio buttons, single and multi selects
setCustomFieldValue('Checkboxes', 'Yes')
// a user custom field
setCustomFieldValue('UserPicker', 'admin')
// system fields
setDescription("A generated description")
// set due date to tomorrow
setDueDate {
set(LocalDate.now().plusDays(1))
}
}Get workflow steps
Get the current action name
You may want to get the current action ID or name for a number of reasons, for example for tracking or auditing purposes, you may want to log the action name each time the post function runs.
You can use the following code to get the ID of the workflow action:
transientVars["actionId"]You can use the following code to get the name of the workflow action:
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
The placement of your custom function is relative to the built-in post functions that update the issue in the database and reindex affects how you retrieve the current and destination steps. If your function is positioned before these system post functions, it will see the issue's state before the transition. If it's after, it will see the state as updated by the transition.
You can use the following code to return the current status:
issue.status.nameYou can use the following code to get the destination step:
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)Assign an issue to a random member of a role
You may want to automatically assign an issue to a random member of a role. For example, you want to assign a Developers role member to an issue after it transitions from Under Review to Investigate. You can use the following code to assign an issue to a random member in the Developers role:
import com.atlassian.jira.security.roles.ProjectRoleManager
import com.atlassian.jira.component.ComponentAccessor
def roleName = "Developers"
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def role = projectRoleManager.getProjectRole(roleName)
def actors = projectRoleManager.getProjectRoleActors(role, issue.getProjectObject())
def roleUsers = actors.getUsers()
def randomIndex = new Random().nextInt(roleUsers.size() - 1)
def randomUser = roleUsers.getAt(randomIndex)
issue.setAssignee(randomUser)Display a message to the user
Use UserMessageUtil to show a banner notification in the Jira UI after a transition. It is best suited for situations where the user is viewing an issue directly and triggers a transition from the issue view; for example, clicking a transition button on the issue page.
Message types
Four types are available depending on the nature of the message:
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
UserMessageUtil.success("Issue reassigned to the reporter.") // 🟢 green — action completed
UserMessageUtil.info("SLA timer has started.") // 🔵 blue — neutral information
UserMessageUtil.warning("Retrofit codes required.") // 🟡 yellow — user action needed
UserMessageUtil.error("Could not notify downstream system.") // 🔴 red — something went wrongBest practices for UserMessageUtil
We recommend the following best practices:
- Use
UserMessageUtilwhen you know the user will be transitioning from the issue view and want a lightweight, non-intrusive notification. - For transitions that may happen from the board or Create dialogue, use
issue.addComment()instead. Comments are written directly to the issue and are always visible regardless of how the transition was triggered. - For the most reliable experience, combine both:
UserMessageUtilfor users on the issue view, and a comment as a fallback.
The following example reassigns the issue to the reporter on transition. It displays a notification if the user has the issue open, and adds a comment to the issue as a fallback so there is always a visible record of the action regardless of how the transition was triggered.
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
issue.set {
setAssignee(issue.reporter)
}
UserMessageUtil.success("Issue reassigned to ${issue.reporter?.displayName}.")
issue.addComment("🔄 Automatically reassigned to reporter on transition to In Progress.") {
setDispatchEvent(false)
}