How to move JSM users between organisations in bulk
Share on socials
How to move JSM users between organisations in bulk
Managing your organisation groups in Jira Service Management just got easier! This step-by-step guide shows you how to move user accounts in bulk, using ScriptRunner for Jira Cloud.
Working in Jira Service Management (JSM), you'll no doubt be familiar with the handy feature that allows you to group customers into separate organisations. JSM also offers the option to automate this process based on email domains, streamlining the process of managing your customers.
But what happens when a company begins to grow and the org chart evolves, and those groups become outdated? With Jira's native capabilities, you can move users between organisation groups individually, but that's going to get tedious quickly when you need to move more than a handful at a time.
So, how do you move JSM users between organisations in bulk? Enter ScriptRunner for Jira Cloud!
Tutorial: Move JSM users between organisations in bulk
Here's a step-by-step guide to facilitate the bulk movement of customers from one organisation to another (even if they have different email domains) using ScriptRunner for Jira Cloud.
How it works
The script performs these steps:
Retrieve all users (customers) within a specified service desk organisation.
Iterate through each customer within the organisation, and verify if any customer email matches any of the provided email domains.
If a customer's email matches the provided domain, that customer is added to the target service desk organisation.
After each matching customer is successfully added to the target service desk organisation, the customer is then removed from the source organisation.
The script will ignore customer accounts that don't match the email domains specified to be moved, and also ignore any internal customers who have hidden their email addresses via account preferences. See the instructions section to optionally retrieve these hidden emails through organisation permissions.
What you'll need
For the script to work, you must have a user account in a user group with at least 'User (Agent)' product role access.
Optional: Organisation ID and API key to retrieve hidden emails, if your JSM project includes internal customers. Find more information about this here.
The script we've shared below!
Example script
/*
* Date Authored: 2024-11-19
* Date Updated : 2025-06-22
* Author : Adonias Lopez Payan | Technical Consultant | Adaptavist, Inc.
* Author : Max Lim | Customer Success Manager | Adaptavist, Inc.
* Description : This console script moves users from one JSM organisation to another, given they match any of the listed domains.
* Usage : Note: Ideally, test the script in a sandbox instance first.
Minimum requirement:
1. A user account in a user group with "User (agent)" product role access.
Line 26) Provide the email of a user account that at least in a user group with "User (agent)" product role access
Line 27) Provide an API token associated to that account:
https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
Line 28 & 29) Use empty strings, "", or optionally provide the organisation ID and an API key (must be without scopes),
for retrieving email addresses of internal customers set hidden in personal preference:
https://support.atlassian.com/organization-administration/docs/manage-an-organization-with-the-admin-apis/
External customer vs internal customers:
https://support.atlassian.com/jira-service-management-cloud/docs/what-are-service-desk-customers-and-organizations/
Line 30) Provide a list of email domains of customers to be moved to the target organisation
Line 31) Provide the name of the source organisation to move users from
Line 32) Provide the name of the target organisation to move users to
* Version : v1.3.1
*/
/********** User Input Start **********/
final USERNAME = BOT_USER_EMAIL // Authentication details can be saved as Script Variables:
final API_TOKEN = BOT_USER_API_TOKEN // https://docs.adaptavist.com/sr4jc/latest/features/script-variables
final ORG_ID = YOUR_ORG_ID // Use empty string, "", if not needed, ie. no internal customers
final ORG_API_KEY = YOUR_ORG_API_KEY // Use empty string, "", if not needed, ie. no internal customers
final DOMAINS_TO_MOVE = ["@example.com",] // An empty string, "", means move all
final SOURCE_ORG_NAME = "Just Org"
final TARGET_ORG_NAME = "Another Org"
/********** User Input End **********/
def getAllJsmAuth = { String url ->
def items = []
def start = 0
def isLastPage = false
while (!isLastPage) {
def authResponse = get(url).queryString('start', start)
.basicAuth(USERNAME, API_TOKEN)
.asObject(Map)
assert authResponse.status >= 200 && authResponse.status <= 300
items.addAll(authResponse.body["values"])
isLastPage = authResponse.body['isLastPage']
start = start + (authResponse.body['limit'] as Integer)
}
items
}
// Main logic
def organisations = getAllJsmAuth("rest/servicedeskapi/organization")
def sourceOrg = organisations.find { it['name'] == SOURCE_ORG_NAME }
def targetOrg = organisations.find { it['name'] == TARGET_ORG_NAME }
if (!sourceOrg) {
logger.info "$SOURCE_ORG_NAME organization Not Found"
return
}
if (!targetOrg) {
logger.info "$TARGET_ORG_NAME organization Not Found"
return
}
def sourceOrgCustomers = getAllJsmAuth("/rest/servicedeskapi/organization/${sourceOrg['id']}/user")
def sourceOrgEmailHiddenCustomers = sourceOrgCustomers.findAll { !it['emailAddress'] }
if (ORG_ID && ORG_API_KEY && sourceOrgEmailHiddenCustomers) {
// From documentation, following API supports returning 10000 users in one call, no pagination needed:
// https://developer.atlassian.com/cloud/admin/organization/rest/api-group-users/#api-v1-orgs-orgid-users-search-post
def getUsersResponse = post("https://api.atlassian.com/admin/v1/orgs/$ORG_ID/users/search")
.header('Authorization', "Bearer $ORG_API_KEY")
.header('Content-Type', 'application/json')
.body([
accountIds: sourceOrgEmailHiddenCustomers.collect { it['accountId'] },
expand: ['EMAIL']
])
.asObject(Map)
assert getUsersResponse.status >= 200 && getUsersResponse.status <= 300
def users = getUsersResponse.body['data']
sourceOrgCustomers.each { customer ->
if (!customer['emailAddress']) customer['emailAddress'] = users.find { it['accountId'] == customer['accountId'] }['email']
}
}
def domainMatchedSourceOrgCustomers = sourceOrgCustomers.findAll { user -> DOMAINS_TO_MOVE.any { user['emailAddress'].toString().endsWith(it) } }
if (!domainMatchedSourceOrgCustomers) {
logger.info "No matched customers from $SOURCE_ORG_NAME organization found with domains: $DOMAINS_TO_MOVE"
return
}
logger.info("Moving following customers from $SOURCE_ORG_NAME to $TARGET_ORG_NAME:")
domainMatchedSourceOrgCustomers.each { logger.info "${it['displayName']}: ${it['accountId']}" }
// Choose to logging out email addresses instead:
// domainMatchedSourceOrgCustomers.each { logger.info "${it['emailAddress']}" }
// There is no limit on how many customers can be added and deleted in one request from API reference, no pagination needed:
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-post
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-delete
// Add first, delete later, so that the customers will always be in at least one organization
def addCustomersToTargetOrgResponse = post("rest/servicedeskapi/organization/${targetOrg['id']}/user")
.header("Content-Type", "application/json")
.body([
accountIds: domainMatchedSourceOrgCustomers.collect { it['accountId'] }
])
.asObject(Map)
assert addCustomersToTargetOrgResponse.status >= 200 && addCustomersToTargetOrgResponse.status <= 300
def deleteCustomersFromSourceOrgResponse = delete("rest/servicedeskapi/organization/${sourceOrg['id']}/user")
.header("Content-Type", "application/json")
.body([
accountIds: domainMatchedSourceOrgCustomers.collect { it['accountId'] }
])
.asObject(Map)
assert deleteCustomersFromSourceOrgResponse.status >= 200 && deleteCustomersFromSourceOrgResponse.status <= 300
Instructions
Let's get your customers moving! Here's what you need to do:
In ScriptRunner for Jira Cloud, it's best practice to create Script Variables to save the user email, API token, and the optional organisation ID and API Key. For example:
Name the script variable: CLOUD_PAT.
Copy and paste the API token into the Script Variable value.
2. Provide the information needed in the following lines:
Line 26: Provide the email of a user account in a user group with at least 'User (Agent)' product role access.
Line 27: Provide an API token associated with that account. More information on Atlassian API tokens here.
Line 28 and 29: Use empty strings ("") or optionally provide the organisation ID and an API key (must be without scopes) for retrieving email addresses of internal customers set hidden in personal preference. More information on this scenario here.
Line 30: Provide a list of email domains of customers to be moved to the target organisation.
Line 31: Provide the name of the source organisation to move users from.
Line 32: Provide the name of the target organisation to move users to.
3. Click Run to execute the script.
4. Click the Logs tab below the script console to see the output listing the users that were moved successfully! 🎉
Need some help?
Our customer success team is on hand to make sure your ScriptRunner journey is smooth sailing. If you need support with implementing this script, or for a different use case, just book some time to chat to us.
With your company growing and your customer base expanding, you have a lot of plates to keep spinning. Keeping your JSM organisations up to date doesn't need to be one of them. Let ScriptRunner for Jira Cloud make your day a little easier!
Want more ways to save time and effort in Jira?
Get that good-night's-sleep feeling with more ScriptRunner for Jira Cloud tutorials. Next up: how you can sync Groups with the Team field in Jira Cloud for seamless work management.