How to Showcase Unused Jira Schemes in Confluence

This article provides a practical guide to integrating Jira with Confluence to display inactive Jira schemes on Confluence pages. By leveraging the Jira and Confluence APIs within the ScriptRunner for Jira add-on, we’ll walk through retrieving and displaying inactive permission and workflow schemes – but this approach can be used for any Jira scheme.

Why is this useful?

This setup gives Jira admins a clear, centralized overview of all unused or outdated configurations, making it easier to manage them directly within a Confluence space. With this real-time insight into inactive schemes, admins can quickly identify and remove unused or outdated configurations, helping to maintain a clean, efficient system.

Additionally, Confluence pages displaying these inactive schemes can be exported to PDF or Word documents, making it easy to share or archive documentation. This capability supports audit processes, project reviews, and compliance checks, ensuring teams are always aligned with up-to-date configurations and best practices across Jira projects.

Before you start

To get started, make sure you have the following in place:

  • A new Confluence space, which you could name something like Inactive Jira Schemes, although any name will work.
  • Two new Confluence pages within that space. They don’t need content or even titles at this stage, but make sure to save their page IDs. You can find the page ID in the URL when you open the page: https://{your_instance}.atlassian.net/wiki/spaces/{space_key}/pages/{pageId}
  • An API token. You can generate a new token here.

Script configuration

The scripts are set up as ScriptRunner Scheduled Jobs. To create a new Scheduled Job, go to Jira Settings and navigate to Apps. In the left sidebar, under ScriptRunner, find and select Scheduled Jobs.

To create new Scheduled Job, click on button Create Scheduled Job. For the job names in our examples, consider names like Inactive Workflow Schemes and Inactive Permission Schemes. Make sure to enable your scheduled jobs so it’s actually active. For the execution schedule, feel free to customize it. For this type of script, I would suggest once per day or even once per week. For the script executor, make sure that selected user has all required permissions (Jira/org admin).

Script for Inactive permission schemes:

def email = 'your_email'
def password = "your_api_token"
def pageId = page_id
def instance = "your_org.atlassian.net"

def projectResult = get('/rest/api/3/project/search')
        .header('Content-Type', 'application/json')
        .asObject(Map)

def projects = projectResult.body.values
def total = projectResult.body.total
def maxResults = projectResult.body.maxResults

def projectPermissionSchemes = []

if(total > maxResults) {
    checkForProjectSchemes(projects, projectPermissionSchemes)
    def startAt = 50
    total += 50
    while(total >= startAt) {
        projectResult = get('/rest/api/3/project/search')
            .header('Content-Type', 'application/json')
            .queryString('startAt', startAt)
            .asObject(Map)
        projects = projectResult.body.values
        checkForProjectSchemes(projects, projectPermissionSchemes)
        startAt += 50    
    }
} else {
    checkForProjectSchemes(projects, projectPermissionSchemes)
}

def inactiveSchemes = getInactivePermissionSchemes(projectPermissionSchemes)
def pageVersion = getPageVersion(email, password, instance, pageId) + 1
updateConfluencePage(pageId, pageVersion, inactiveSchemes, email, password, instance)

def checkForProjectSchemes(List projects, List projectPermissionSchemes) {
    projects.each { project ->
        def projectId = project.id
        def permissionScheme = getProjectPermissionScheme(projectId)
        if(!projectPermissionSchemes.contains(permissionScheme)) {
            projectPermissionSchemes.add(permissionScheme)
        }
    }
}

def getProjectPermissionScheme(String id) {

    def permission = get("/rest/api/3/project/${id}/permissionscheme")
        .header('Content-Type', 'application/json')
        .asObject(Map).body.name
    return permission    
}

def getInactivePermissionSchemes(List activePermissionSchemes) {
    def unusedSchemes = []
    def allSchemes = get("/rest/api/3/permissionscheme")
        .header('Content-Type', 'application/json')
        .asObject(Map).body.permissionSchemes

    allSchemes.each { scheme ->
        logger.info("Scheme: " + scheme.name)
        if(!activePermissionSchemes.contains(scheme.name)) {
            unusedSchemes.add(scheme.name)
        }
    }
    return unusedSchemes
}

def getPageVersion(String email, String password, String instance, int pageId) {

    def pageResult = get("https://${instance}/wiki/api/v2/pages/${pageId}?body-format=storage")
        .header('Content-Type', 'application/json')
        .basicAuth(email, password)
        .asObject(Map)
    return pageResult.body.version.number    
}

def updateConfluencePage(int pageId, int version, List unusedPermissionSchemes, String email, String password, String instance) {

    def value = ""
    unusedPermissionSchemes.each { scheme ->
        value += "- ${scheme}\n"
    }

    def params = [
        id: "${pageId}",
        type : "page",
        title: "Jira - Inactive permission schemes",
        status: "current",
        version: [
            number: version
        ],
        body : [
            storage: [
                value: "<p>${value}</p>",
                representation: "storage"
            ]
        ]
    ] as Map

    def response = put("https://${instance}/wiki/api/v2/pages/${pageId}")
        .header('Content-Type', 'application/json')
        .basicAuth(email, password)
        .body(params)
        .asObject(Map).body
    return response.body    
}

Script for Inactive Workflow Schemes:

def email = 'your_email'
def password = "your_api_token"
def pageId = page_id
def instance = "your_org.atlassian.net"

def projectResult = get('/rest/api/3/project/search')
        .header('Content-Type', 'application/json')
        .asObject(Map)

def projects = projectResult.body.values
def total = projectResult.body.total
def maxResults = projectResult.body.maxResults

def projectWorkflowSchemes = []

if(total > maxResults) {
    checkForProjectSchemes(projects, projectWorkflowSchemes)
    def startAt = 50
    total += 50
    while(total >= startAt) {
        projectResult = get('/rest/api/3/project/search')
            .header('Content-Type', 'application/json')
            .queryString('startAt', startAt)
            .asObject(Map)
        projects = projectResult.body.values
        checkForProjectSchemes(projects, projectWorkflowSchemes)
        startAt += 50    
    }
} else {
    checkForProjectSchemes(projects, projectWorkflowSchemes)
}

def unusedSchemes = []
def start = 0
def inactiveSchemes = getInactiveWorkflowSchemes(projectWorkflowSchemes, start, unusedSchemes)
def pageVersion = getPageVersion(email, password, instance, pageId) + 1
updateConfluencePage(pageId, pageVersion, inactiveSchemes, email, password, instance)

def checkForProjectSchemes(List projects, List projectWorkflowSchemes) {
    projects.each { project ->
        def projectId = project.id
        def workflowSchemes = getProjectWorkflowSchemes(projectId)
        workflowSchemes.each { ws ->
            if(!projectWorkflowSchemes.contains(ws)) {
                projectWorkflowSchemes.add(ws)
            }
        }
    }
}

def getProjectWorkflowSchemes(String id) {

    def schemes = []
    def workflowSchemes = get("/rest/api/2/workflowscheme/project")
        .header('Content-Type', 'application/json')
        .queryString("projectId", id)
        .asObject(Map).body.values

    workflowSchemes.each { ws ->
        schemes.add(ws.workflowScheme.name)
    }
    return schemes    
}

def getInactiveWorkflowSchemes(List activeWorkflowSchemes, int startAt, List unusedSchemes) {
    def schemeResponse = get("/rest/api/2/workflowscheme")
        .header('Content-Type', 'application/json')
        .queryString("startAt", startAt)
        .asObject(Map).body

    def allSchemes = schemeResponse.values
    def total = schemeResponse.total
    def maxResults = schemeResponse.maxResults
    def isLast = schemeResponse.isLast
    allSchemes.each { scheme ->
        logger.info("Scheme: " + scheme.name)
        if(!activeWorkflowSchemes.contains(scheme.name)) {
            unusedSchemes.add(scheme.name)
        }
    }
    if(!isLast) {
        startAt += 50
        getInactiveWorkflowSchemes(activeWorkflowSchemes, startAt, unusedSchemes)
    }
    else {return unusedSchemes}
}

def getPageVersion(String email, String password, String instance, int pageId) {

    def pageResult = get("https://${instance}/wiki/api/v2/pages/${pageId}?body-format=storage")
        .header('Content-Type', 'application/json')
        .basicAuth(email, password)
        .asObject(Map)
    return pageResult.body.version.number    
}

def updateConfluencePage(int pageId, int version, List unusedWorkflowSchemes, String email, String password, String instance) {

    def value = ""
    unusedWorkflowSchemes.each { scheme ->
        value += "- ${scheme}\n"
    }

    logger.info("Value is: " + value)

    def params = [
        id: "${pageId}",
        type : "page",
        title: "Jira - Inactive Workflow Schemes",
        status: "current",
        version: [
            number: version
        ],
        body : [
            storage: [
                value: "<p>${value}</p>",
                representation: "storage"
            ]
        ]
    ] as Map

    def response = put("https://${instance}/wiki/api/v2/pages/${pageId}")
        .header('Content-Type', 'application/json')
        .basicAuth(email, password)
        .body(params)
        .asObject(Map).body
    return response.body    
}

After you copy and paste these scripts, you can test them by clicking the Run now button.

After execution was successful, open Confluence and check pages that should’ve been updated. Page for inactive permission schemes should look something like this:

And page for inactive workflow schemes should look something like this:

These are very simple pages with only title and a list of all inactive schemes but you can always customize scripts to display it differently.

Keep in mind

  • This setup is achieved with ScriptRunner for Jira, but you can create a similar solution using a Script Job in ScriptRunner for Confluence.
  • If you plan to extend or modify your script with additional features, keep in mind that it times out after 240 seconds. Complex scripts may require optimization or breaking up into smaller, scheduled jobs to stay within this limit.

Reach out!

I hope this article was helpful to you! If you have any additional questions, want to dive deeper into the topic, or have ideas for improvement, I’d love to hear from you.

You can find links to my LinkedIn profile and email at the bottom of the page, or feel free to reach out via the Contact page.