We couldn't export Issue Security Level Permission: fixing the Jira JCMA error step by step
Mihai Perdum
Author
14 min readSeptember 25, 2026
Key takeaways
Atlassian's knowledge base says the error means an issue still references an issue security level that no longer exists. Its fix is to find those issues and set their security column to null or to a valid level.
Setting security to null makes the issue visible to everyone with Browse Projects in that project. Pick a real level where the issue was meant to be restricted.
No Atlassian source says whether the number in the error is a level id or a grant-row id, so the queries here check both tables.
Grants based on a user or group custom field only migrate if the field type is on JCMA's supported list. Otherwise they are dropped without an error, and on Cloud an issue whose only grant was dropped disappears for everyone that grant covered, admins included.
Copy product data lists work item security level, scheme and permission as copied. It documents nothing about custom-field grants or orphaned levels, so verify on the destination with the REST check in this tutorial.
JCMA stops a project migration with "We couldn't export Issue Security Level Permission 10001. Reason: java.lang.NullPointerException. [JCMA 000]" when an issue still points at a security level that no longer exists. The full line in the migration log looks like this, where TEST is the project key:
text
1ERROR TEST project-export We couldn't export Issue Security Level Permission 10001. Reason: java.lang.NullPointerException. [JCMA 000]
That is the example in Atlassian's own knowledge base article for this error. The fix it gives is three SQL statements with no walkthrough, and the one public Community thread about it, a question with this exact error, ends with the asker saying "I checked the database tables but couldn't find a table or so that looked like a "Issue Security Level Permission"". Atlassian has a ticket open about exactly that, MIG-1815, titled "The migration error for issue security level should be more clear".
By the end of this tutorial you will have found every issue that points at a missing security level, fixed them, checked the security grants that JCMA drops without failing, and proven on the Cloud side that a restricted issue is visible to the people it should be and hidden from everyone else. The last section covers the Cloud-to-Cloud route, where JCMA is not involved at all.
Note
Prerequisites — Jira Data Center or Server with read and write access to its database, and a backup taken before any change (Atlassian's own warning: "Always backup your instance database before any changes, or first try the changes in a staging environment"). For the Cloud checks, an API token for a Jira Cloud admin on the destination site, plus curl. We have no Data Center instance, so the SQL here was run on a local PostgreSQL 16 database with a stub of the relevant Jira tables, not on a live Jira. The Cloud steps were run on our own test site on 25 September 2026.
Why the issue security level migration error happens
Two tables matter, and the error's wording makes it easy to look in the wrong one.
schemeissuesecuritylevels holds the levels themselves, one row per level, with the scheme it belongs to. schemeissuesecurities holds the grants: who can see an issue at a given level. Each grant row has the level id in its security column, a sec_type, and a sec_parameter such as a group name. The issue itself carries its level in jiraissue.security.
Atlassian's article describes the cause in one line: "It indicates that there is a reference to an Issue Security Level Permission in a Jira Issue that no longer exists." Its first diagnostic step looks the id up in schemeissuesecuritylevels, so it treats the number as a level id. My guess is that a level removed directly in the database or by an old cleanup is the usual way in; Atlassian does not say. Either way, any issue that still carries that id in its security column has nothing to export.
Here is the part I could not settle. The error says "Permission", and a grant row is the thing that looks like a permission. JCMA also has a separate code, JCMA 133, "Group reference not found (issue security level permission)", which is clearly about grants. No Atlassian page, ticket or doc I could find says which table the number in the JCMA 000 line comes from, and ids in both tables start around 10000, so a number can exist in either. So the queries below check both. If your id turns out to be a grant row, you will know from step 4.
There is a second variant of the same problem that prints a different line. MIG-1815 quotes it:
text
1ERROR PROJA project-export We couldn't export Issue PROJA-351. Reason: java.lang.IllegalStateException: Entity mri:mig:jira/classic:issueSecurityLevel:10010 is expected to be exported, but it was not. [JCMA 000]
Same family, but here JCMA names the issue and the level for you, which saves you steps 2 and 3.
A NullPointerException in a JCMA log is not always this problem. In the Community thread, an Atlassian answer notes that "each instance could be encountering an NPE on many different root causes". There is also a knowledge base article called JCMA Project Migration: Handling NullPointerException, but it covers a different error on the project-import side, about a permission scheme, so do not follow its fix for this one.
The line you are fixing here has three things in it: project-export, the words "Issue Security Level Permission", and an id. Write down the project key and every id. The Community asker had six such lines for one project, with five distinct ids (10080 twice, then 10110, 11294, 11295 and 11596), so expect more than one. Run the steps below once per id.
JCMA's troubleshooting page does not cover this one. It has an entry for JCMA 133, where you fix the reference in the Pre-migration checks screen ("Select Fix remediable errors in the Data preparation section of the Pre-migration checks page"), choose a valid entity id and rerun. JCMA 000 has no entry and no such screen, so this one is fixed in the database.
Find and fix the orphaned security level on Data Center
These are the queries I ran, exactly as printed, on the stub database. The stub was seeded with one deleted level (10001), two issues still pointing at it, one grant row on the deleted level, and two custom-field grants. That is our seed, not real data, but it has the same shape as the case Atlassian describes. Swap TEST and 10001 for your project key and the id from your log.
1
Back up the database
take a full backup, or restore one to a staging instance and run everything there first. Every query up to step 6 is read-only; step 6 writes.
2
Check whether the id exists as a level
this is Atlassian's step 1: SELECT * FROM schemeissuesecuritylevels WHERE id=10001; An empty result means the level is gone, which confirms the orphan.
3
List the security column for every issue in the project
Atlassian's step 2, which returns every issue in the project so you can look for the id by eye. On a big project, use the orphan finder in the next step instead.
4
Run the orphan finders
one query returns only the issues whose level no longer exists, a second returns grant rows on a missing level, and a third checks whether the id is a grant-row id rather than a level id.
5
Check the custom-field grants
join the grants to customfield and read the customfieldtypekey of every field used in a grant. Anything that is not on JCMA's supported list will not migrate (next section).
6
Update the orphaned issues
Atlassian's step 3: set jiraissue.security to null, or to the correct level, for each issue found in step 4. Then run the orphan finder again. It should return zero rows.
7
Re-run the JCMA pre-migration checks
Atlassian's article ends at the UPDATE and says nothing about what to do next. Re-running the checks and the export is my advice, not theirs.
Here is the script for steps 2 to 6, in one file. The labels inside it keep Atlassian's numbering because this is the file exactly as I ran it: its "Step 1" is step 2 above, "Step 2" is step 3, "Step 2b", "Step 2c" and the grant-id check are step 4, the two custom-field queries are step 5, and its "Step 3" is step 6.
sql
1\echo '--- Step 1 (KB): does the ID from the error exist as a level?'2SELECT*FROM schemeissuesecuritylevels WHERE id=10001;3\echo '--- Step 2 (KB): security column of every issue in the project'4SELECT jiraissue.id
5, jiraissue.issuenum
6, jiraissue.security
7FROM jiraissue
8JOIN project ON jiraissue.project = project.id
9WHERE project.pkey ='TEST';10\echo '--- Step 2b: only the issues pointing at a level that no longer exists'11SELECT p.pkey ||'-'|| i.issuenum AS issue_key, i.id, i.security
12FROM jiraissue i
13JOIN project p ON p.id = i.project
14LEFTJOIN schemeissuesecuritylevels l ON l.id = i.security
15WHERE i.security ISNOTNULLAND l.id ISNULL16ORDERBY i.id;17\echo '--- Step 2c: grant rows (schemeissuesecurities) pointing at a missing level'18SELECT s.id AS permission_id, s.scheme, s.security AS level_id, s.sec_type, s.sec_parameter
19FROM schemeissuesecurities s
20LEFTJOIN schemeissuesecuritylevels l ON l.id = s.security
21WHERE l.id ISNULL;22\echo '--- Is 10001 a grant-row id? (checks the other reading of the error)'23SELECT*FROM schemeissuesecurities WHERE id=10001;24\echo '--- Custom-field grants and their field type keys'25SELECT s.id AS permission_id, l.name ASlevel, s.sec_type, s.sec_parameter, cf.cfname, cf.customfieldtypekey
26FROM schemeissuesecurities s
27JOIN schemeissuesecuritylevels l ON l.id = s.security
28JOIN customfield cf ON s.sec_parameter ='customfield_'|| cf.id
29ORDERBY s.id;30\echo '--- KB (unsupported custom fields) query'31select customfieldtypekey,cfname from customfield where cfname in('SecureGroupPicker','SecureMultiUser');32\echo '--- Step 3 (KB): UPDATE then re-check'33BEGIN;34UPDATE jiraissue
35SET security =NULL36WHERE id =20002;37UPDATE jiraissue
38SET security =NULL39WHERE id =20004;40SELECT p.pkey ||'-'|| i.issuenum AS issue_key, i.id, i.security
41FROM jiraissue i
42JOIN project p ON p.id = i.project
43LEFTJOIN schemeissuesecuritylevels l ON l.id = i.security
44WHERE i.security ISNOTNULLAND l.id ISNULL45ORDERBY i.id;46COMMIT;
1--- Step 1 (KB): does the ID from the error exist as a level?
2 id | name | scheme
3----+------+--------
4(0 rows)
56--- Step 2 (KB): security column of every issue in the project
7 id | issuenum | security
8-------+----------+----------
9 20001 | 1 | 10000
10 20002 | 2 | 10001
11 20003 | 3 |
12 20004 | 4 | 10001
13(4 rows)
1415--- Step 2b: only the issues pointing at a level that no longer exists
16 issue_key | id | security
17-----------+-------+----------
18 TEST-2 | 20002 | 10001
19 TEST-4 | 20004 | 10001
20(2 rows)
2122--- Step 2c: grant rows (schemeissuesecurities) pointing at a missing level
23 permission_id | scheme | level_id | sec_type | sec_parameter
24---------------+--------+----------+----------+-----------------
25 10101 | 10000 | 10001 | group | old-contractors
26(1 row)
2728--- Is 10001 a grant-row id? (checks the other reading of the error)
29 id | scheme | security | sec_type | sec_parameter
30----+--------+----------+----------+---------------
31(0 rows)
3233--- Custom-field grants and their field type keys
34 permission_id | level | sec_type | sec_parameter | cfname | customfieldtypekey
35---------------+----------+----------+-------------------+-------------------+-------------------------------------------------------------------
36 10102 | Partners | userCF | customfield_10200 | SecureMultiUser | com.atlassian.jira.plugin.system.customfieldtypes:multiuserpicker
37 10103 | Partners | groupCF | customfield_10201 | SecureGroupPicker | com.example.vendor:secure-group-picker
38(2 rows)
3940--- KB (unsupported custom fields) query
41 customfieldtypekey | cfname
42-------------------------------------------------------------------+-------------------
43 com.atlassian.jira.plugin.system.customfieldtypes:multiuserpicker | SecureMultiUser
44 com.example.vendor:secure-group-picker | SecureGroupPicker
45(2 rows)
4647--- Step 3 (KB): UPDATE then re-check
48BEGIN
49UPDATE 1
50UPDATE 1
51 issue_key | id | security
52-----------+----+----------
53(0 rows)
5455COMMIT
To confirm the fix, read the last block: the orphan finder that returned TEST-2 and TEST-4 before the UPDATE returns zero rows after it. That empty result proves something because the same query, on the same database, found both issues a moment earlier.
A few things to know before you run this on a real instance.
The || concatenation is PostgreSQL. It also works on Oracle. On MySQL use CONCAT('customfield_', cf.id), and on SQL Server use 'customfield_' + CAST(cf.id AS varchar). I only ran the PostgreSQL version.
The custom-field query filters on sec_parameter = 'customfield_' || cf.id rather than on sec_type. Atlassian's own knowledge base shows custom-field grants storing customfield_<id> in sec_parameter. The userCF and groupCF values I seeded in sec_type are my inference, and I have not seen them in a live Data Center database, so do not filter on them.
If step 4 returns a grant row on a missing level, like 10101 here, that row points at nothing. Atlassian does not document a fix for it, so check it in the admin UI and your staging copy before you delete anything.
Choose between null and a real level carefully. Atlassian's Data Center article Issue cannot be viewed and Permission Violation error is thrown warns that setting security to null "will make the issue accessible by all users that has the Browse Project permission in the project", and tells you to set the level again through the UI afterwards. If TEST-2 was restricted for a reason, give it a level that still exists, not null. The UPDATE in step 6 widens access, not narrows it.
This is the quieter half of the problem, and on the Cloud side it is the one that bites.
Atlassian's article JCMA doesn't migrate issue security level permission with unsupported custom fields (for Jira 7.6 and higher, JCMA 1.10.5 or higher) says: "JCMA doesn't migrate unsupported custom fields linked to issue security level permission for the Group custom field value and User customer field value." The article has a list of supported field type keys. The core user and group pickers are on it (userpicker, multiuserpicker, grouppicker, multigrouppicker), as are a set of Jira Service Management and Advanced Roadmaps fields. A third-party picker, whose key starts with the vendor's namespace, is not.
To be precise about what that article says: the grant is "not migrated". It does not say the migration fails, and I found nothing that ties an unsupported field to the JCMA 000 error. You get no error. The grant just does not arrive.
In the stub output above, SecureGroupPicker has the key com.example.vendor:secure-group-picker, which is not on the list, so that grant would not migrate. SecureMultiUser uses the core multiuserpicker and would.
Atlassian gives two fixes before migrating. Either delete the affected grant, migrate, and "Recreate the issue security level permission in cloud", or swap the unsupported field for a supported one on the level and then migrate. If you have already migrated, the article's instruction is: "re-create the issue security level permission in cloud."
Verify issue security levels on Cloud after the migration
Whichever route you took, the destination is where you confirm that the fix worked. I ran this on our test site with a throwaway project (LZSEC), a multi-user picker field, and a scheme with two levels: "Secure-field", granted to that field's users, and "Reporter-only". Three issues: LZSEC-1 at Secure-field with my account in the field, LZSEC-2 at Secure-field with the field empty, and LZSEC-3 with no level. Everything was deleted afterwards.
1
Set up credentials
export your site and an admin's API token so the commands below run as printed.
2
List the issue security schemes
confirm the migrated scheme exists on the destination and note its id.
3
List the levels and their members
read every grant on every level and compare against your Data Center list from the previous section.
4
Fetch a restricted issue as a member
the positive control. A 200 with the level name proves the grant works.
5
Fetch a restricted issue as a non-member
the negative control. Use a second account's API token, one you know is outside the level, or an issue whose grant provably excludes you. A 404 on an issue you know exists then proves the restriction works.
6
Re-create any missing custom-field grant
add it back to the level, then repeat steps 4 and 5.
The commands below carry the ids from our test project, which no longer exists. Replace 10420 with your scheme id from the first call, 10379 and 10333 with your level and grant ids from the member call, customfield_11629 with your field id, and the LZSEC keys with a restricted issue of your own. On any other site the ids as printed will return 404, which is not a restriction working, just a wrong id.
On our site the levels call returned 10380 "Reporter-only" and 10379 "Secure-field", and the member call returned two grants. Trimmed to the fields that matter:
LZSEC-1 came back 200 with security "Secure-field". LZSEC-2, same level but with the field empty, came back 404 even though I am a site admin. In our test the admin was the non-member, because the only grant on that level was the field and my account was not in it. After a real migration your admin may be covered by a group grant, so use a second user's token for this call:
text
1{"errorMessages":["Issue does not exist or you do not have permission to see it."],"errors":{}}
That pair is the whole check. The 200 shows a member can see the issue. The 404 on an issue you know exists shows the restriction is enforced. A 404 on its own proves nothing, because a typo in the key gives you the same response.
Then I recreated the failure the previous section describes. I deleted the custom-field grant (DELETE /rest/api/3/issuesecurityschemes/10420/level/10379/member/10333, 204), which is the state an unsupported field leaves you in after JCMA. LZSEC-1, the issue I could see a moment earlier, now returned the same 404. With its only grant gone, a level hides the issue from everyone not otherwise granted, admin included. That is what your users will report after the migration: issues that were there yesterday are gone.
To put it back, add the grant to the level:
bash
1curl-s-u"$AUTH"-X PUT -H'Content-Type: application/json'\2"$SITE/rest/api/3/issuesecurityschemes/10420/level/10379/member"\3-d'{"members":[{"type":"userCF","parameter":"customfield_11629"}]}'
It returned 204. To confirm it, repeat the checks: LZSEC-1 returned 200 with "Secure-field" again, and LZSEC-2 still returned 404, so the negative control held.
Things the API taught me on the way:
The type name differs between write and read. When I created the grant with "type":"userCustomField", the name the read endpoint returns, I got {"errorMessages":["Type userCustomField isn't a valid type."]}. Creating it with "type":"userCF" worked, and reading it back returns userCustomField.
The levelId filter on the member endpoint looked like it was ignored in one of my calls. Filter by schemeId and sort the levels yourself.
JQL lags. project = LZSEC AND level = "Secure-field" came back empty about five seconds after I created the issues and correct about thirty-five seconds later. One observation, but enough to not trust an empty JQL result straight after a migration or a grant change.
Setting a level on an issue needs the Set Issue Security permission. On our site the admin did not have it under the shared default permission scheme, so the security field did not appear at all. I used a throwaway permission scheme for the test project rather than edit the shared one.
Assigning a security scheme to a project through PUT /rest/api/3/issuesecurityschemes/project takes {"schemeId":"10420","projectId":"11601"} and answers 303, because it runs asynchronously. {"issueSecuritySchemeId": ...} returns "Invalid request payload".
Issue security levels in Cloud to Cloud migration (Copy product data)
Cloud to Cloud has no JCMA, so you will not see this error, and you cannot fix anything with SQL, because there is no database you can reach. Atlassian's page What data is copied lists, under Jira project data, "✅ Work item security: Level, Scheme, and Permission", alongside "✅ Permission schemes" and "✅ Users, groups, and teams from active directories". It also notes that workflows and permission schemes not linked to any project won't be migrated.
It says nothing about grants based on custom fields, nothing about third-party pickers, and nothing about a level that exists in the scheme but has no grants. Neither does Atlassian's troubleshooting page for data transfers, Troubleshoot issues with transferring app data, though that page is about app data. I have not run a cross-site copy for this piece, so I cannot tell you what it does with those edge cases. Nothing I found says either way.
Two documented behaviours do change what you check. First, ids move. What happens when you copy data says custom field identifiers "such as customfield_10456" are "not guaranteed to be the same" between sites. That passage is written about sandboxes, but the rule matters for security grants: a user-custom-field grant on the destination must name the destination's field id, not the source's. Second, groups with the same name merge: "If a group with the same name already exists on your sandbox, we'll merge the two groups." A grant to a group can end up admitting more people than it did on the source. I wrote up that trap on its own in Migrate Jira users and groups without escalation.
So on Cloud to Cloud the procedure is the Cloud section above, run on the destination: list the schemes, list every level's members, compare against the source (the same REST calls work on the source site), and do the member and non-member fetch for at least one issue per level. Where a custom-field grant is missing or names a field that does not exist on the destination, re-create it with the destination's field id.
What this does not cover
The Data Center half was run on a stub schema with only the columns these queries use, on PostgreSQL, and not against a live Jira. The table and column names come from Atlassian's own knowledge base SQL. Whether the number in the error is a level id or a grant id is undocumented. The Cloud half was run live. The Cloud-to-Cloud half rests on one line in Atlassian's docs plus the REST check, not on a copy we ran.
You can now find every issue that points at a missing security level with one orphan-finder query, fix it with Atlassian's UPDATE, and prove the fix by re-running the same query to zero.
You check both tables, because nothing documents whether the id in the error is a level or a grant row.
You know that null opens the issue to everyone with Browse Projects, and you pick a real level where the issue was meant to be restricted.
You find unsupported custom-field grants before JCMA drops them, and you re-create them on Cloud if it already has.
You verify on the destination with a member fetch that returns 200 and a non-member fetch that returns 404, on both JCMA and Copy product data migrations.
Not covered: MySQL, SQL Server and Oracle runs of the SQL, a live Data Center instance, and a real Copy product data run.
Confluence Space Permissions After Migration: Audit Who Lost or Gained Access