This blocked our automation workflows and created unnecessary manual work just to remove those inactive users.
This small utility script has saved us countless hours of debugging automation failures and makes our user management much smoother.
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.user.search.UserSearchService
import com.atlassian.jira.bc.user.search.UserSearchParams
// ====== CONFIGURE ======
final String FIELD_IDENTIFIER = "customfield_15501" // User picker (single/multi)
final String JQL = '"Visible to" in inactiveUsers() and project = "Support" and created >= 2025-10-09 and created <= 2025-10-10' // adjust as needed
final String ACT_AS_USERNAME = "jira.automation"
// ========================
// Resolve actor
def userManager = ComponentAccessor.userManager
def actingUser = Users.getByName(ACT_AS_USERNAME)
if (!actingUser) return "ERROR: Could not find user '${ACT_AS_USERNAME}'."
// Services
def issueManager = ComponentAccessor.issueManager
def customFieldManager = ComponentAccessor.customFieldManager
def searchService = ComponentAccessor.getComponent(SearchService)
def jqlParser = ComponentAccessor.getComponent(JqlQueryParser)
def userSearchService = ComponentAccessor.getComponent(UserSearchService)
// Get Field Object
CustomField cf = customFieldManager.getCustomFieldObject(FIELD_IDENTIFIER)
if (cf == null) return "ERROR: Field '${FIELD_IDENTIFIER}' not found."
// Parse + validate JQL
def query = jqlParser.parseQuery(JQL)
def validation = searchService.validateQuery(actingUser, query)
if (validation.hasAnyErrors()) {
return "Invalid JQL: ${validation.getErrorMessages()}"
}
// Build explicit UserSearchParams (don't use variable name 'params')
def searchParams = UserSearchParams.builder()
.allowEmptyQuery(true)
.includeActive(true)
.includeInactive(true)
.maxResults(1000000) // large value; adjust if needed
.build()
// Now call findUsers with the searchParams
def allUsers = userSearchService.findUsers("", searchParams)
def inactiveUsers = allUsers.findAll { u -> u != null && !u.isActive() }
def inactiveIds = inactiveUsers.collect { u ->
// some returned objects might not be ApplicationUser - safe cast
try { (u as ApplicationUser).getId() as Long } catch (e) { null }
}.findAll { it != null } as Set<Long>
// Search issues
def results = searchService.search(actingUser, query, PagerFilter.unlimitedFilter)
def issues = results.getResults()
def updated = []
issues.each { issue ->
MutableIssue mi = issueManager.getIssueByCurrentKey(issue.getKey())
if (mi == null) return
def fieldValue = mi.getCustomFieldValue(cf)
if (fieldValue == null) return
def currentUsers = (fieldValue instanceof Collection) ? fieldValue : [fieldValue]
def kept = currentUsers.findAll { obj ->
ApplicationUser u = (obj instanceof ApplicationUser) ? obj : null
return (u != null && !inactiveIds.contains(u.getId() as Long))
}
if (kept.size() != currentUsers.size()) {
def typeKey = cf.getCustomFieldType().getKey().toLowerCase()
def isMulti = typeKey.contains("multi")
mi.setCustomFieldValue(cf, isMulti ? kept : (kept ? kept[0] : null))
issueManager.updateIssue(actingUser, mi, EventDispatchOption.ISSUE_UPDATED, false)
updated << mi.getKey()
}
}
return "✅ Removed inactive users from ${updated.size()} issues (acting as ${ACT_AS_USERNAME}). Issues: ${updated.join(', ')}"
1 comment