If you have ever tried to migrate a Jira Product Discovery project from one Jira Cloud instance to another using the Data Transfer, you have probably noticed a massive gap: Your Ideas migrate, but all of your Custom Views and Idea Insights are left behind.
Because JPD uses a proprietary, internal GraphQL API (Project Polaris) instead of the standard Jira REST API, native migration tools do not pick up the view configurations or the insights attached to the ideas.
Faced with having to manually copy-paste hundreds of insights and rebuild 50+ complex views by hand, I decided to reverse-engineer the GraphQL API to automate the migration.
Here is the methodology, the API endpoints, and the major "gotchas" we found along the way so you can build your own migration scripts.
To migrate Marketplace app data, contact the respective Marketplace Partner.
You cannot simply copy a View's JSON from Instance A and push it to Instance B. Under the hood, Views save columns, groupings, and matrix axes using raw internal IDs (e.g., customfield_10011 or 712020:12926ecf-0bec... for a User ID).
When you move to a new Cloud tenant, Custom Field IDs and User Account IDs often change. If you push an old ID to the new instance, the View will corrupt. Before migrating anything, your script must build Cross-Tenant Translation Maps:
User Remapping: Export the user list from both instances, match them by email address, and create a dictionary mapping the Old Account ID to the New Account ID.
Field Remapping: Query the standard /rest/api/3/field endpoint on both instances. Match the fields by their exact plain-text names, and map the old customfield_X to the new customfield_Y.
Your migration script will need to recursively pass your extracted data through these maps, replacing old IDs with new ones before injecting them into the target site.
To migrate Insights, your script should query the polarisInsights GraphQL endpoint on the source site, map the Ideas to their new issue keys, and push them to the target site.
The GraphQL Read Query:
query jira_polaris_ReadInsights($project: ID!, $container: ID) {
polarisInsights(project: $project, container: $container) {
description
account { name }
snippets { oauthClientId url data properties }
}
}
The GraphQL Write Mutation:
mutation jira_polaris_CreateInsight($input: CreatePolarisInsightInput!) {
createPolarisInsight(input: $input) { success }
}
Two critical lessons learned here:
Broken Media: Insights use Atlassian Document Format (ADF). If an insight contains an image, it references a media ID tied to the old tenant. Pushing this directly will result in a broken image or a GraphQL rejection. Your script must traverse the ADF payload and strip out media, mediaSingle, and mediaGroup nodes before pushing.
Attribution Loss: When you push an insight to the new site via API, Jira will list you (the API token owner) as the author. To preserve history, extract the original author's name (account{name}) from the Read query and inject a new text block at the top of the Insight ADF payload (e.g., π€ Original Author: [Name]).
Extracting the configurations for your Lists, Boards, and Matrixes requires querying the entire Project Snapshot.
The GraphQL Read Query:
query jira_polaris_ProjectQuery_reduced($projectId: ID!) {
polarisProject(id: $projectId) {
viewsets {
name
views {
name
visualizationType
layoutType
fields { id }
groupBy { id }
verticalGroupBy { id }
tableColumnSizes { field { id } size }
matrixConfig { axes { dimension field { id } reversed } }
}
}
}
}The GraphQL Write Mutation:
mutation jira_polaris_CreateView($input: CreatePolarisViewInput!) {
createPolarisView(input: $input) { success }
}The Ultimate "Gotcha" - Read vs. Write Filter Schemas:
Through GraphQL introspection, we discovered a massive quirk in Atlassian's backend. The way Jira Reads a View Filter is structurally completely different from the way it expects you to Write a View Filter. If you download a view and try to push its exact filter block back to the createPolarisView endpoint, Jira will reject it.
The Pragmatic Solution:
Do not try to translate the complex filter logic programmatically. In your migration script, simply drop the filter block from the extracted payload entirely before you send the Create mutation.
Your script will perfectly recreate your 50+ Views, their names, their emojis, all of your custom columns, your matrix axes, and your board groupings.
Once the views are created via the API, your product team just needs to spend a few minutes clicking the "Filter" button in the UI to manually re-apply the filter logic (e.g., "Status = Now"). It is infinitely faster than rebuilding the entire layout from scratch!
By combining dynamic field/user mapping with these undocumented GraphQL endpoints, we successfully migrated 100% of our Insights and perfectly cloned over 40 complex Views automatically.
If you are writing your own scripts, remember to handle Atlassian's 429 Too Many Requests limits with exponential backoff!
Has anyone else tackled JPD migrations recently? Let me know if you end up using these endpoints to build your own tools or if you find a way to crack the Write-Filter schema!
Henrique Bittencourt
0 comments