Cloud migrations have two sides that both demand serious attention. There is the data transfer itself -- moving issues, pages, attachments, users, and configuration from Data Center to Cloud -- and there is everything that comes before and around it. Neither side is simple.
The Cloud Migration Assistant handles the mechanics of moving data across, and Atlassian continues to invest in this area with programs like FastShift. But "handles the mechanics" does not mean you press a button and walk away. The assistant transfers data. It does not guarantee that every issue, every page, every attachment, every permission grant arrives intact and complete on the other side. Getting to zero data loss -- genuinely zero -- is the migration expert's real job, and it takes hands-on validation at every step. Content that silently fails to transfer, users that lose access because a group mapping broke, attachments that time out on large files, pages where macros render as blank boxes -- these things happen during the transfer itself, not just before or after it.
The other side is the assessment: understanding what you actually have, how tangled the configuration is, what will not migrate automatically, and how much effort the whole thing takes. When an assessment is thorough, the migration becomes predictable. You know the number of workflows that need manual rebuilding. You know which marketplace apps have no Cloud equivalent. You know the real licensed user count, not the inflated one from the wrong database table. You know how many terabytes of attachments are sitting there and which individual files might cause timeouts.
When the assessment is shallow -- which is the default -- the timeline keeps shifting because new things keep turning up, and the validation work balloons because you did not know what to check for.
This guide is about doing the assessment properly. It covers three ways to gather data: direct SQL queries against the Jira or Confluence database, REST API calls you can run from your own machine, and ScriptRunner Groovy scripts you can run from the ScriptRunner console if you have it installed. Between these three approaches, you can extract everything you need without relying on someone else's tooling or waiting for a vendor engagement.
The MAGIC Framework
Before gathering any data, it helps to know what you are looking for. The MAGIC framework organises the assessment into five areas. Each one addresses a different category of risk, and each one can be rated Low, Medium, or High complexity based on the data you collect.
M -- Migration Strategy. Are you migrating everything at once, or in batches? How many batches, and in what order? Which projects depend on each other and need to move together? The answer depends on the data you gather from the other four areas.
A -- Apps, Integrations & Customisations. This is usually where the biggest surprises are. Which marketplace apps have Cloud equivalents? Which ones require complete manual rebuilds? What happens to your ScriptRunner Groovy scripts, your JMWE post-functions, your JSU conditions? What integrations (webhooks, application links, REST-based automations) break when URLs change?
G -- Growth & Scalability. How large is the instance right now? How many issues, pages, attachments, users? What Cloud tier does that map to? Are there specific projects or spaces that are disproportionately large and might need special handling during migration?
I -- Identity Management. How is authentication configured today? LDAP, SAML, Crowd, internal directory? What changes when you move to Atlassian Cloud and Atlassian Access? How will user provisioning and deprovisioning work going forward?
C -- Compliance & Security. Data residency requirements, anonymous access review, permission model changes between Data Center and Cloud, and any regulatory constraints that affect where data can live.
Each rating is only useful if it is backed by specific numbers. A rating of "HIGH" for Apps complexity means nothing unless you can say "23 marketplace apps installed, 4 are DC-only, 3 require manual rebuilds totalling approximately 430 post-functions across 120 workflows." That specificity is what makes the migration predictable.
The MAGIC5 Document
The output of the assessment is what we call the MAGIC5 document. It is a structured summary that consolidates everything into a single view that stakeholders can read and act on.
It starts with an instance overview: product version, deployment type, database engine, the accurate licensed user count, project and space counts, issue and page counts, total attachment storage, and the number of marketplace apps installed. This section uses the exact numbers from the data gathering phase -- not estimates, not round numbers from the admin console.
Then it covers each of the five MAGIC areas with a complexity rating and the specific data that supports it. For the Apps section, this means listing every installed marketplace app alongside its migration path: does it migrate automatically, does it need manual configuration in Cloud, does it need a complete rebuild, or is there no Cloud version at all?
It includes a risk register with concrete entries based on the query results. Something like "47 workflows contain JMWE post-functions that will not migrate automatically and require manual analysis and rebuild, estimated 70-140 hours" is useful. Something like "some apps may need attention" is not.
And it ends with a recommended migration strategy -- big bang or phased batching -- along with the batch composition if phased, the pre-migration cleanup list, and rough effort estimates.
The rest of this guide covers how to gather the data that feeds into this document.
Gathering Data: SQL Queries
These queries run directly against the Jira or Confluence database. They are written for PostgreSQL. If you are running MySQL, Oracle, or SQL Server, you will need to adjust a few syntax details -- STRING_AGG becomes GROUP_CONCAT in MySQL, LISTAGG in Oracle -- but the table and column names are the same across all supported databases.
You need read access to the database. If you do not have it, skip ahead to the REST API and ScriptRunner sections -- those approaches work through the application layer and do not require database access.
Project Inventory (Jira)
This tells you how many projects exist, how large each one is, when it was last active, and which schemes are attached to it. The scheme information is particularly important because projects that share a workflow scheme generally need to migrate together in the same batch.
1SELECT2 p.pkey AS project_key,3 p.pname AS project_name,4 p.projecttype AS project_type,5 pc.cname AS category,67(SELECTCOUNT(*)FROM jiraissue ji WHERE ji.project = p.id)AS issue_count,89(SELECTCOUNT(*)FROM jiraissue ji
10JOIN issuestatus s ON ji.issuestatus = s.id
11WHERE ji.project = p.id AND s.statuscategory =2)AS open_issues,1213 p.created AS created,14(SELECTMAX(updated)FROM jiraissue WHERE project = p.id)AS last_activity,1516(SELECTCOUNT(*)FROM component WHERE project = p.id)AS component_count,17(SELECTCOUNT(*)FROM projectversion WHERE project = p.id)AS version_count,1819(SELECT ws.name FROM workflowscheme ws
20JOIN nodeassociation na ON na.sink_node_id = ws.id
21WHERE na.source_node_id = p.id
22AND na.source_node_entity ='Project'23AND na.sink_node_entity ='WorkflowScheme'24LIMIT1)AS workflow_scheme,2526(SELECT ps.name FROM permissionscheme ps
27JOIN nodeassociation na ON na.sink_node_id = ps.id
28WHERE na.source_node_id = p.id
29AND na.source_node_entity ='Project'30AND na.sink_node_entity ='PermissionScheme'31LIMIT1)AS permission_scheme,3233(SELECT ns.name FROM notificationscheme ns
34JOIN nodeassociation na ON na.sink_node_id = ns.id
35WHERE na.source_node_id = p.id
36AND na.source_node_entity ='Project'37AND na.sink_node_entity ='NotificationScheme'38LIMIT1)AS notification_scheme
3940FROM project p
41LEFTJOIN app_user au ON p.lead = au.user_key
42LEFTJOIN cwd_user u ON au.lower_user_name = u.lower_user_name
43LEFTJOIN projectcategory pc ON p.pcategory = pc.id
44ORDERBY issue_count DESC;45
Projects with zero issues or no activity in over a year are candidates for archival before migration. Every project you skip is time saved, both during the migration and in validation afterwards.
The scheme columns use a table called nodeassociation that trips people up. Jira does not store most scheme relationships as foreign keys on the project table. Instead, it uses this generic junction table where source_node_entity = 'Project' and sink_node_entity is the scheme type. If you do not know this table exists, you will waste time looking for a workflow_scheme_id column that is not there.
Licensed User Count (Jira)
This is the single most important number for cost planning. It directly determines which Cloud license tier you need.
Do not count rows in cwd_user. That table contains everyone who has ever existed in any connected directory: inactive accounts, service accounts, users deleted from LDAP, people who logged in once three years ago. The real licensed user count comes from the licenserolesgroup table, which maps user groups to license roles. A user only consumes a license if they belong to a mapped group, and both the user and the directory are active.
1-- Jira Software licensed users2SELECTCOUNT(DISTINCT u.lower_user_name)AS jira_software_licensed_users
3FROM cwd_user u
4JOIN cwd_membership m ON u.id = m.child_id AND u.directory_id = m.directory_id
5JOIN licenserolesgroup lrg ON LOWER(m.parent_name)= LOWER(lrg.group_id)6JOIN cwd_directory d ON m.directory_id = d.id
7WHERE d.active ='1'AND u.active ='1'8AND lrg.license_role_name ='jira-software';910-- Jira Service Management licensed users11SELECTCOUNT(DISTINCT u.lower_user_name)AS jsm_licensed_users
12FROM cwd_user u
13JOIN cwd_membership m ON u.id = m.child_id AND u.directory_id = m.directory_id
14JOIN licenserolesgroup lrg ON LOWER(m.parent_name)= LOWER(lrg.group_id)15JOIN cwd_directory d ON m.directory_id = d.id
16WHERE d.active ='1'AND u.active ='1'17AND lrg.license_role_name ='jira-servicedesk';1819-- Breakdown by product20SELECT21 lrg.license_role_name AS product,22COUNT(DISTINCT u.lower_user_name)AS licensed_users
23FROM cwd_user u
24JOIN cwd_membership m ON u.id = m.child_id AND u.directory_id = m.directory_id
25JOIN licenserolesgroup lrg ON LOWER(m.parent_name)= LOWER(lrg.group_id)26JOIN cwd_directory d ON m.directory_id = d.id
27WHERE d.active ='1'AND u.active ='1'28GROUPBY lrg.license_role_name
29ORDERBY licensed_users DESC;
Getting this wrong -- and it is easy to get wrong -- means buying a Cloud tier that is either too expensive or too small. The difference between a 500-user tier and a 2,000-user tier is significant, and the answer is often not what people expect when they look at the raw cwd_user count.
Workflow Complexity (Jira)
This is where the real cost of a migration hides. Jira stores the entire workflow definition as an XML document in the descriptor column of the jiraworkflows table. That XML contains every transition, condition, validator, and post-function, including the Java class names of marketplace app extensions.
The Cloud Migration Assistant does not migrate post-functions from apps like JMWE, JSU, or ScriptRunner. Every one of those post-functions needs to be manually analysed, documented, and rebuilt in Cloud. If you do not count them before committing to a timeline, you have no idea how much work is actually involved.
1SELECT2 w.workflowname AS workflow_name,34(SELECTCOUNT(DISTINCT p.id)5FROM project p
6JOIN nodeassociation na ON na.source_node_id = p.id
7AND na.source_node_entity ='Project'8AND na.sink_node_entity ='WorkflowScheme'9JOIN workflowscheme ws ON ws.id = na.sink_node_id
10JOIN workflowschemeentity wse ON wse.scheme = ws.id
11WHERE wse.workflow = w.workflowname)AS project_count,1213(SELECTCOUNT(*)14FROM jiraissue ji
15JOIN os_wfentry wfe ON ji.workflow_id = wfe.id
16WHERE wfe.name = w.workflowname)AS issue_count,1718(LENGTH(w.descriptor)- LENGTH(REPLACE(w.descriptor,'<action ','')))19/ LENGTH('<action ')AS transition_count,20(LENGTH(w.descriptor)- LENGTH(REPLACE(w.descriptor,'<condition ','')))21/ LENGTH('<condition ')AS condition_count,22(LENGTH(w.descriptor)- LENGTH(REPLACE(w.descriptor,'<validator ','')))23/ LENGTH('<validator ')AS validator_count,24(LENGTH(w.descriptor)- LENGTH(REPLACE(w.descriptor,'<function ','')))25/ LENGTH('<function ')AS postfunction_count,2627CASEWHEN w.descriptor LIKE'%com.onresolve%'28THEN'Yes'ELSE'No'ENDAS has_scriptrunner,29CASEWHEN w.descriptor LIKE'%com.innovalog%'OR w.descriptor LIKE'%jmwe%'30THEN'Yes'ELSE'No'ENDAS has_jmwe,31CASEWHEN w.descriptor LIKE'%com.googlecode.jsu%'32THEN'Yes'ELSE'No'ENDAS has_jsu
3334FROM jiraworkflows w
35ORDERBY project_count DESC NULLS LAST;
The plugin key patterns are worth committing to memory. ScriptRunner uses com.onresolve, JMWE uses com.innovalog, and JSU uses com.googlecode.jsu. That last one is not intuitive and people regularly get it wrong, which means they miss JSU post-functions in their count and end up with a timeline that is too short.
Custom Fields (Jira)
Custom field proliferation is one of the most common problems in mature instances. Teams create new fields instead of reusing existing ones, and over years the instance accumulates hundreds or thousands of them.
The issues_with_value column is the one that matters most. Fields with zero usage can be deleted before migration. Fields where the field_type contains com.onresolve are ScriptRunner scripted fields that will not function in Cloud without a rewrite. Fields with the same name but different IDs should be consolidated before migration to avoid confusion.
Attachment Storage (Jira)
Large attachments slow down migration and can cause timeouts during transfer. You need to know both the total footprint and which specific files are problematic.
1SELECT2COUNT(*)AS total_attachments,3ROUND(SUM(filesize)/1024.0/1024.0/1024.0,2)AS total_gb,4ROUND(AVG(filesize)/1024.0,2)AS avg_size_kb,5COUNT(*) FILTER (WHERE filesize >10485760)AS files_over_10mb,6COUNT(*) FILTER (WHERE filesize >52428800)AS files_over_50mb
7FROM fileattachment;89SELECT10 p.pkey ||'-'|| ji.issuenum AS issue_key,11 fa.filename,12 fa.mimetype,13ROUND(fa.filesize /1024.0/1024.0,2)AS size_mb,14 fa.created
15FROM fileattachment fa
16JOIN jiraissue ji ON fa.issueid = ji.id
17JOIN project p ON ji.project = p.id
18WHERE fa.filesize >1048576019ORDERBY fa.filesize DESC20LIMIT50;
Schemes (Jira)
Scheme sprawl builds up quietly over years. People create new permission schemes, notification schemes, and workflow schemes for specific projects, and they never get cleaned up. The result is more schemes than projects, many of them near-identical copies.
Schemes with project_count = 0 are unused and can be deleted. Schemes used by only one project are worth comparing against similar schemes to see if consolidation is possible.
Filters and Dashboards (Jira)
Filters with subscriptions are particularly important because those subscriptions trigger email notifications. If a filter breaks after migration, people silently stop getting updates they depend on. Nobody notices until someone asks why their weekly report stopped arriving.
1SELECT2 sr.filtername AS filter_name,3 u.display_name AS owner,4 sr.reqcontent AS jql,5CASE6WHENEXISTS(SELECT1FROM sharepermissions sp
7WHERE sp.entityid = sr.id
8AND sp.entitytype ='SearchRequest'9AND sp.sharetype ='global')THEN'Global'10WHENEXISTS(SELECT1FROM sharepermissions sp
11WHERE sp.entityid = sr.id
12AND sp.entitytype ='SearchRequest'13AND sp.sharetype ='group')THEN'Group'14ELSE'Private'15ENDAS share_type,16(SELECTCOUNT(*)FROM favouriteassociations fa
17WHERE fa.entityid = sr.id
18AND fa.entitytype ='SearchRequest')AS favourite_count,19(SELECTCOUNT(*)FROM filtersubscription fs
20WHERE fs.filter_i_d = sr.id)AS subscription_count
21FROM searchrequest sr
22LEFTJOIN app_user au ON sr.authorname = au.user_key
23LEFTJOIN cwd_user u ON au.lower_user_name = u.lower_user_name
24ORDERBY favourite_count DESC;
Confluence: Space Inventory
1SELECT2 s.spacekey,3 s.spacename,4 s.spacetype,56(SELECTCOUNT(*)FROM content c
7WHERE c.spaceid = s.spaceid
8AND c.contenttype ='PAGE'9AND c.content_status ='current')AS page_count,1011(SELECTCOUNT(*)FROM content c
12WHERE c.spaceid = s.spaceid
13AND c.contenttype ='BLOGPOST'14AND c.content_status ='current')AS blog_count,1516(SELECTROUND(COALESCE(SUM(a.filesize),0)/1024.0/1024.0/1024.0,2)17FROM attachments a
18JOIN content c ON a.pageid = c.contentid
19WHERE c.spaceid = s.spaceid)AS attachment_gb,2021(SELECTMAX(c.lastmoddate)FROM content c
22WHERE c.spaceid = s.spaceid)AS last_activity
2324FROM spaces s
25WHERE s.spacestatus ='CURRENT'26ORDERBY page_count DESC;
Personal spaces (spacetype = 'personal') tend to be the largest category by count but lowest in business value. Whether they all need to migrate is a decision worth making explicitly.
Confluence: Licensed Users
Confluence uses a completely different mechanism for counting licensed users than Jira does. Instead of licenserolesgroup, it uses the SPACEPERMISSIONS table and the USECONFLUENCE permission type.
There is also a difference that trips people up every time: Confluence uses 'T' and 'F' (character strings) for boolean active flags, not 1 and 0 (numbers) like Jira. Copy a Jira user query into Confluence without changing this and you get zero results.
1SELECTCOUNT(DISTINCT u.lower_user_name)AS confluence_licensed_users
2FROM cwd_user u
3JOIN cwd_membership m ON u.id = m.child_user_id
4JOIN cwd_group g ON m.parent_id = g.id
5JOIN SPACEPERMISSIONS sp ON g.group_name = sp.PERMGROUPNAME
6JOIN cwd_directory d ON u.directory_id = d.id
7WHERE sp.PERMTYPE ='USECONFLUENCE'8AND u.active ='T'9AND d.active ='T';
Confluence: Macro Usage
Macros are the Confluence equivalent of workflow post-functions in Jira -- the thing most likely to break during migration and the thing most likely to be missed. Confluence stores page content as XML in the bodycontent table, and macros are embedded as XML tags within that content.
1SELECT2 macro_name,3COUNT(*)AS total_usage,4COUNT(DISTINCT spaceid)AS space_count,5COUNT(DISTINCT contentid)AS page_count
6FROM(7SELECT8 c.contentid,9 c.spaceid,10 UNNEST(REGEXP_MATCHES(11 bc.body,12'<ac:structured-macro[^>]*ac:name="([^"]+)"',13'g'14))AS macro_name
15FROM content c
16JOIN bodycontent bc ON c.contentid = bc.contentid
17JOIN spaces s ON c.spaceid = s.spaceid
18WHERE c.contenttype ='PAGE'19AND c.content_status ='current'20AND s.spacestatus ='CURRENT'21AND bc.body LIKE'%<ac:structured-macro%'22) macros
23GROUPBY macro_name
24ORDERBY total_usage DESC;
Native macros like code, info, note, warning, panel, expand, toc, and children migrate without issues. Third-party macros -- anything with a com. prefix -- need to be checked against the vendor's Cloud marketplace listing individually. Pages using the HTML macro need particular attention because arbitrary HTML and JavaScript execution is heavily restricted in Cloud for security reasons.
Confluence: Anonymous Access
Confluence has a quirk in how it stores anonymous permissions. In the spacepermissions table, an anonymous permission is identified by checking that all three user identifier columns are NULL simultaneously: permusername, permgroupname, and permalluserssubject. Check only one or two and you get false results.
1SELECT2 s.spacekey,3 s.spacename,4 sp.permtype AS anonymous_permission
5FROM spacepermissions sp
6JOIN spaces s ON sp.spaceid = s.spaceid
7WHERE sp.permusername ISNULL8AND sp.permgroupname ISNULL9AND sp.permalluserssubject ISNULL10AND s.spacestatus ='CURRENT'11ORDERBY s.spacekey;
Confluence: The user_mapping Table
One more Confluence gotcha. You cannot join content.creator directly to cwd_user. There is an intermediate table called user_mapping that bridges between user keys and usernames. Skip it and you get zero results or wrong matches.
1SELECT2 c.title,3 u.display_name AS creator
4FROM content c
5JOIN user_mapping um ON c.creator = um.user_key
6JOIN cwd_user u ON um.lower_username = u.lower_user_name
7WHERE c.content_status ='current';
Gathering Data: REST API Scripts
If you do not have direct database access, or you want to supplement the SQL results with data that is easier to get through the application layer, you can use the Jira and Confluence REST APIs. These scripts can be run from any machine that has network access to the instance -- your laptop, a CI server, wherever.
The examples below use curl for clarity, but you can wrap these in any scripting language. The key is that they do not require you to install anything on the Jira or Confluence server itself.
All Projects With Metadata (Jira)
1# Jira: list all user-installed plugins2curl-s-u"$USER:$TOKEN"\3"$BASE_URL/rest/plugins/1.0/?os_authType=basic"\4-H"Accept: application/json"\5| python3 -c" import json, sys data = json.load(sys.stdin) plugins = data.get('plugins', []) user_installed = [p for p in plugins if p.get('userInstalled')] for p in sorted(user_installed, key=lambda x: x.get('name', '')): print(f'{p.get(\"name\", \"??\"):50} {p.get(\"key\", \"\")}') print(f'\nTotal user-installed apps: {len(user_installed)}') "
All Custom Fields (Jira)
This gives you every custom field in the instance along with its type, which is useful for identifying ScriptRunner scripted fields and other marketplace-app-provided fields.
1# Get all projects with issue counts2curl-s-u"$USER:$TOKEN"\3"$BASE_URL/rest/api/2/project?expand=description,lead,url"\4| python3 -m json.tool > projects.json
56# For each project, get issue count via search7forKEYin$(cat projects.json | python3 -c" import json, sys for p in json.load(sys.stdin): print(p['key']) ");do8COUNT=$(curl-s-u"$USER:$TOKEN"\9"$BASE_URL/rest/api/2/search?jql=project=$KEY&maxResults=0"\10| python3 -c "import json,sys; print(json.load(sys.stdin)['total'])")11echo"$KEY: $COUNT issues"12done
All Installed Apps (Jira/Confluence)
This extracts the full list of installed marketplace apps, which feeds directly into the MAGIC "A" (Apps) assessment.
1# Jira: list all user-installed plugins2curl-s-u"$USER:$TOKEN"\3"$BASE_URL/rest/plugins/1.0/?os_authType=basic"\4-H"Accept: application/json"\5| python3 -c" import json, sys data = json.load(sys.stdin) plugins = data.get('plugins', []) user_installed = [p for p in plugins if p.get('userInstalled')] for p in sorted(user_installed, key=lambda x: x.get('name', '')): print(f'{p.get(\"name\", \"??\"):50} {p.get(\"key\", \"\")}') print(f'\nTotal user-installed apps: {len(user_installed)}') "
Workflow Export (Jira)
The REST API can export workflows in XML format, which lets you analyse post-functions without needing direct database access.
1# Get all workflow names2curl-s-u"$USER:$TOKEN"\3"$BASE_URL/rest/api/2/workflow"\4| python3 -c" import json, sys workflows = json.load(sys.stdin) for wf in workflows: print(wf['name']) "> workflow_names.txt
56# Count workflows with JMWE/JSU/ScriptRunner (requires DB or XML export)7# The REST API does not expose workflow XML directly, but you can get8# workflow scheme associations:9curl-s-u"$USER:$TOKEN"\10"$BASE_URL/rest/api/2/workflowscheme"\11| python3 -m json.tool > workflow_schemes.json
Confluence Spaces and Content Counts
1# Get all spaces with metadata2curl-s-u"$USER:$TOKEN"\3"$BASE_URL/rest/api/space?limit=500&expand=description.plain"\4| python3 -c" import json, sys data = json.load(sys.stdin) for s in data['results']: print(f'{s[\"key\"]:15} {s[\"name\"]:50} {s[\"type\"]}') print(f'\nTotal spaces: {data[\"size\"]}') "56# Get page count per space7forKEYin$(curl-s-u"$USER:$TOKEN"\8"$BASE_URL/rest/api/space?limit=500"\9| python3 -c" import json, sys for s in json.load(sys.stdin)['results']: print(s['key']) ");do10COUNT=$(curl-s-u"$USER:$TOKEN"\11"$BASE_URL/rest/api/content?spaceKey=$KEY&type=page&limit=0"\12| python3 -c "import json,sys; print(json.load(sys.stdin).get('size', 0))")13echo"$KEY: $COUNT pages"14done
Gathering Data: ScriptRunner Groovy Scripts
If you have ScriptRunner installed on the Data Center instance, you can run Groovy scripts directly in the ScriptRunner console. This is often the most practical approach because it gives you access to the full Jira or Confluence Java API without needing database credentials or external network access.
Workflow Post-Function Audit (Jira)
This script iterates over every workflow in the instance, parses the XML descriptor, and counts post-functions from JMWE, JSU, and ScriptRunner. This is the same information as the SQL workflow query, but gathered through the application layer.
This is more practical than the SQL approach for many teams because it does not require database access and produces a result you can copy straight into the assessment document.
Custom Field Usage Report (Jira)
1importcom.atlassian.jira.component.ComponentAccessor23def cfManager =ComponentAccessor.getCustomFieldManager()4def searchService =ComponentAccessor.getComponent(5com.atlassian.jira.bc.issue.search.SearchService6)7def user =ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()8def results =newStringBuilder()910results.append("Field Name | Type | Issues Using It\n")11results.append("-"*70+"\n")1213def unusedCount =01415cfManager.getCustomFieldObjects().each { cf ->16 def jql ="cf[${cf.idAsLong}] is not EMPTY"17try{18 def parseResult = searchService.parseQuery(user, jql)19if(parseResult.isValid()){20 def count = searchService.searchCount(user, parseResult.getQuery())21if(count ==0) unusedCount++22 results.append(23"${cf.name.take(40).padRight(40)} | "+24"${cf.customFieldType?.name?.take(20)?.padRight(20)} | "+25"${count}\n"26)27}28}catch(Exception e){29 results.append("${cf.name.take(40).padRight(40)} | ERROR: ${e.message}\n")30}31}3233results.append("\nTotal custom fields: ${cfManager.getCustomFieldObjects().size()}")34results.append("\nUnused fields (0 issues): ${unusedCount}")3536return results.toString()
Group Membership and License Impact (Jira)
This script shows which groups grant license access and how many active users are in each one, which gives you the same information as the licenserolesgroup SQL query but without needing database access.
1importcom.atlassian.jira.component.ComponentAccessor23def groupManager =ComponentAccessor.getGroupManager()4def licenseRoleManager =ComponentAccessor.getComponent(5com.atlassian.jira.license.LicenseRoleManager6)7def results =newStringBuilder()89results.append("License Role | Group | Active Users\n")10results.append("-"*60+"\n")1112licenseRoleManager.getLicenseRoles().each { role ->13 role.getGroups().each { groupName ->14 def group = groupManager.getGroup(groupName)15if(group){16 def members = groupManager.getUsersInGroup(group)17 def activeCount = members.count { it.isActive()}18 results.append(19"${role.name.padRight(25)} | "+20"${groupName.padRight(25)} | "+21"${activeCount}\n"22)23}24}25}2627return results.toString()
Confluence: Space Size and Activity Report
If you have ScriptRunner for Confluence, this script gathers space metrics without database access.
If your Jira or Confluence admin can generate a support zip from the admin console (System > Troubleshooting and support tools > Create support zip), it contains a wealth of structured data that you can parse without needing database access or ScriptRunner.
The support zip contains several files that are directly useful for the assessment:
application.xml contains the full list of installed plugins with their keys, versions, vendors, and enabled/disabled status. Parsing this file gives you the complete marketplace app inventory. It also contains SSO configuration details if SAML or OAuth is set up.
dbconfig.xml (Jira) or confluence.cfg.xml (Confluence) contains the database type and connection details. You need to know the database engine (PostgreSQL, MySQL, Oracle, SQL Server) because it affects which SQL syntax variations you use and whether certain migration features are supported.
Health check results (JSON or text files) contain the output of Jira's built-in health checks. Failed health checks can indicate problems that need to be resolved before migration -- things like index corruption, attachment store inconsistencies, or database schema issues.
Cluster configuration files tell you whether the instance is running as a Data Center cluster and how many nodes are active. This affects the migration approach because you need to understand the deployment topology to plan the cutover.
You can parse these files with any scripting language. The plugin list from application.xml is XML and can be processed with standard XML parsers. The configuration files are either XML or Java properties format. The health check results are typically JSON.
Patterns Worth Looking For
Orphaned Configuration
Projects, schemes, custom fields, and filters that nobody owns or uses but are still sitting in the instance. They inflate your assessment numbers, add noise to every query result, and create unnecessary work during migration. Query for zero-usage items and flag them for cleanup before you start.
The Mega-Workflow
A single workflow with 40+ transitions, dozens of conditions, and post-functions from three different marketplace apps. These take the most time to migrate because they are usually undocumented and the original author is long gone. When you find one, budget time for analysis before rebuilding. Once you understand what the workflow actually does, the rebuild itself is usually straightforward.
Custom Field Sprawl
Hundreds of fields, many sharing the same name but with different IDs, others created for a one-off project and never used again. The custom field inventory query surfaces this, and the issues_with_value column tells you which fields are actually in use and which are dead weight.
Attachment Bloat
Database dumps, video recordings, log files, and other large binaries that nobody accesses anymore. Large attachments slow down migration, cause timeouts during transfer, and consume Cloud storage you pay for. Identify the largest files and the projects they belong to before migration starts.
Permission Complexity
Permission schemes that use dynamic grants -- Reporter, Current Assignee, Project Lead -- combined with project roles that reference groups that reference LDAP directories. This chain of dependencies needs to work correctly in Cloud, and verifying it requires testing specific users against specific projects. It is methodical work, but it is finite and predictable once you have the permission scheme inventory from the queries above.
Migration Strategy
The assessment data determines the strategy.
Big bang -- migrate everything at once -- works for small instances, under 50 projects and under 100K issues. It is simpler to plan but higher risk: if something breaks, everything is affected.
Phased batching is the right approach for medium to large instances. Plan 5-7 production batches plus an initial pilot batch. The composition of each batch matters: projects that share a workflow scheme should be in the same batch, projects with cross-project Agile boards must migrate together, and if you use a test management app with linkages between test cases and requirements across projects, those projects must co-migrate.
Start the pilot with 5-10 low-risk, low-complexity projects. The pilot validates the migration process, uncovers issues with Cloud environment setup, and gives the team experience before they tackle the complex batches.
Pre-Migration Cleanup
Delete unused custom fields. Archive inactive projects. Consolidate duplicate schemes. Remove orphaned components and versions. Install Cloud versions of marketplace apps that require pre-installation (some apps need to exist in Cloud before the migration assistant runs, or their data will not transfer). Configure SSO/SAML in the Cloud environment. Verify domain ownership. Run a test migration and validate the results. Document every JMWE, JSU, and ScriptRunner post-function that will need manual rebuilding. Review anonymous access on Confluence spaces. Contact owners of heavily-used filters and dashboards to coordinate post-migration testing.
Estimating Effort
These numbers come from real-world migrations. They are raw engineering hours with no contingency buffer -- add 20-30% on top for unknowns.
Basic project migration runs about 0.5 hours per project. Rebuilding a workflow that uses JMWE or JSU runs about 4 hours per workflow, plus 1-1.5 hours for each individual post-function within that workflow. ScriptRunner Groovy scripts take about 16 hours each because the Cloud API is fundamentally different from the Server/DC API and they require complete rewrites.
Reviewing automation rules takes about 0.5 hours each. Validating a critical marketplace app takes about 8 hours per app. Reviewing custom fields runs about 0.25 hours each. Confluence space migration is about 0.5 hours per space, and reviewing pages with HTML macros is about 0.25 hours per affected page.
Beyond per-unit work, budget about 5 days for SSO configuration, 3 days for Cloud environment setup, 10 days for a full pilot migration cycle, and 15 days for final cutover and stabilisation. And do not forget post-migration validation -- verifying that data arrived intact, attachments are accessible, permissions are correct, and nothing was silently dropped. The validation effort scales with the size of the instance, and it is not optional if zero data loss is the goal.
Wrapping Up
A migration becomes predictable when you know exactly what you are working with. The assessment gives you that knowledge, and the validation work afterwards confirms that nothing was lost along the way.
Count licensed users through licenserolesgroup for Jira and SPACEPERMISSIONS for Confluence -- not through cwd_user. Parse workflow XML for post-function counts. Map schemes to projects through nodeassociation. Remember the Confluence gotchas: 'T'/'F' for booleans, the user_mapping bridge table, the three-NULL-column check for anonymous access.
Use SQL if you have database access, REST APIs if you do not, and ScriptRunner if it is already installed. Between these three approaches, you can build a complete picture of the instance without depending on anyone else's timeline.
Clean up before you migrate. Every unused field, orphaned project, and empty scheme you remove makes the migration simpler and the target environment cleaner.
The goal is not a perfect assessment. The goal is zero data loss -- every issue, every page, every attachment, every permission arriving intact in Cloud. A thorough assessment tells you what to expect, and that means you know exactly what to validate once the data lands on the other side. No surprises during the transfer, no content silently left behind.