Skip to main content
Reduce manual work: How to integrate Confluence data with Jira fields
Share on socials
A Confluence table on the left and a Jira window on the right, with pink circular arrows in the middle showing data syncing

Reduce manual work: How to integrate Confluence data with Jira fields

In a perfect world, all systems are interoperable and data flows freely between them. Unfortunately, few of us live in that perfect world! Teams in a typical enterprise environment are often using dozens of systems, many completely siloed, in their day-to-day workflows. Sound more familiar?
So, how can you make sure that data is up to date and accurate across systems—especially when client billing depends on it?
This tutorial will show you just one way to keep that vital data synced between your Confluence and Jira instance using ScriptRunner for Confluence Cloud.
A wrench icon on a teal cog

The problem

Picture this example: your company develops digital training courses for clients. Each course has a unique code, which allows your teams to match the course—and, crucially, the associated billing—with the correct client. However, your current system relies on a spreadsheet to create and manage course codes. The process involves a lot of entering and updating data manually, so it's time-consuming, and errors are common.
Ideally, you'd like course codes to be automatically available within your Jira instance as a custom field value, as soon as they've been created, so the teams working on client courses can just select the relevant code from a drop-down. That way, client work could be tracked more easily and reliably billed back to the correct account.
Due to the legacy nature of your Accounts Payable system, it's not possible to automatically create course codes and have them available to poll via an API, and it wouldn't be efficient to have your Jira admins spending time manually checking for new codes and constantly updating the custom field options.
Light bulb icon

The solution

Fortunately, you have ScriptRunner for Confluence Cloud in your toolkit.
Here's the plan: when a new course project begins, the Project Manager assigned to its production will create a course code and add it to a table on a specific Confluence page. That page update triggers a ScriptRunner Listener. The script reads the updated table data and automatically updates the options available for a specified custom field in Jira, based on what has been added to or deleted from the Confluence table.

Tutorial: How to integrate Confluence table data with Jira custom fields

How it works
  • This script is a Listener that runs when a specific page (as determined by the page title) is updated.
  • The script reads through the table on the specific page, capturing data from the second column in each row.
  • It ignores the table header page and ensures that any blank spaces are trimmed and inconsistent capitalisation is normalised before the values are stored as a list.
  • It then uses those values to update the enabled options in a custom field in the client's Jira instance.
  • It assumes the target custom field context is the global context.
A white exclamation mark in a teal warning triangle

Good to know

The script adds new table values as new options, and any deleted values become disabled options. If a table value is reentered, it will be reenabled.

What you'll need

  • For this script to work, you must have a user account with 'Product admin' access to Jira. In this scenario, it's best practice to create a service account for this purpose, rather than using a personal account.
  • An API token for the user account above. You can create a token here before you get started.
  • The space key and page title for the source page in Confluence, and the custom field ID for the target field in Jira. Watch the recording we've included below to see where to find these.
  • The example script below!

Example script

import groovy.json.JsonSlurper

final SPACE_KEY = '~5f89cb49c07c880075bba803'
final PAGE_TITLE = 'Test Title'
// Update the select list customfield id from your Jira instance
final JIRA_CUSTOMFIELD_ID = 'customfield_10158'

if (page.spaceKey != SPACE_KEY || page.title != PAGE_TITLE) return

def getPageResponse = get("/wiki/api/v2/pages/${page.id}")
    .queryString('body-format', 'atlas_doc_format')
    .asObject(Map)
    .body

def pageStorage = getPageResponse['body']['atlas_doc_format']['value'] as String
def contents = new JsonSlurper().parseText(pageStorage)['content'] as List

// Work through the page contents to read the only table, and values in second column, not including the table header row
def rows = contents.find { it['type'] == 'table' }['content'] as List
def tableCellRows = rows.findAll { it['content'].any { it['type'] == 'tableCell' } }
def cellValues = tableCellRows.collect { it['content'][1]['content'][0]['content']['text'] }.flatten() as List<String>

// Data validation to ensure now spaces or inconsistent capitalization
def optionNames = cellValues
    *.trim() // Remove starting and trailling spaces if any
    *.replaceAll(/\s+/, ' ') // Replace all type of white spaces with single space
    .collect { it.split(' ').collect{ it.capitalize() }.join(' ') } // Capitalize first letter of each word

// Calling Jira REST APIs from Confluence requires authentication
// In practice, this involves creating and authenticated through a bot user
// Documentation on creating an API token:
// https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
// Save the token and access it in script using ScriptRunner's Script Variable feature:
// https://docs.adaptavist.com/sr4cc/latest/features/script-variables

def jiraBaseUrl = baseUrl.replace('/wiki', '')

def contexts = getAllJira("$jiraBaseUrl/rest/api/3/field/${JIRA_CUSTOMFIELD_ID}/context") as List
def globalContext = contexts.find { it['isGlobalContext'] }

def currentOptions = getAllJira("$jiraBaseUrl/rest/api/3/field/${JIRA_CUSTOMFIELD_ID}/context/${globalContext['id']}/option") as List
def currentEnabledOptions = currentOptions.findAll { !it['disabled'] } as List
def currentDisabledOptions = currentOptions - currentEnabledOptions

def newOptionNames = optionNames - (currentOptions.collect { it['value'] })
def enablableOptions = currentDisabledOptions.findAll { it['value'] in optionNames }
def disablableOptions = currentEnabledOptions.findAll { !(it['value'] in optionNames) }

logger.warn "newOptionNames: $newOptionNames"
logger.warn "enablableOptions: $enablableOptions"
logger.warn "disablableOptions: $disablableOptions"

// Do not allow more than 5 item updates at a time
// A safeguard in case the table is accidentally distorted and updated, 
// This will prevent the script from massively changing the Jira options in case of user error
// You can set the number that matches your use case
assert newOptionNames.size() <= 5 && enablableOptions.size() <= 5 && disablableOptions.size() <= 5

if (newOptionNames) {
    post("$jiraBaseUrl/rest/api/3/field/${JIRA_CUSTOMFIELD_ID}/context/${globalContext['id']}/option")
        .header('Content-Type', 'application/json')
        .basicAuth(BOT_USER_EMAIL, BOT_USER_API_TOKEN)
        .body([
            options: newOptionNames.collect { [value: it ]}
        ])
        .asObject(Map)
}

if (enablableOptions + disablableOptions) {
    put("$jiraBaseUrl/rest/api/3/field/${JIRA_CUSTOMFIELD_ID}/context/${globalContext['id']}/option")
        .header('Content-Type', 'application/json')
        .basicAuth(BOT_USER_EMAIL, BOT_USER_API_TOKEN)
        .body([
            options: (enablableOptions + disablableOptions).each { it['disabled'] = !it['disabled'] }
        ])
        .asObject(Map)
}

// Work with paginations on Jira REST APIs
def getAllJira(String url) {
    def items = []
    def startAt = 0
    def isLast = false
    while (!isLast) {
        def response = get(url).queryString('startAt', startAt)
            .basicAuth(BOT_USER_EMAIL, BOT_USER_API_TOKEN)
            .asObject(Map).body
        items.addAll(response["values"])
        isLast = response['isLast']
        startAt = startAt + (response['maxResults'] as Integer)
    }
    items
}

Instructions

1. It's best practice to create Script Variables to save the user email and API token. For example:
  • Name the script variable.
  • Copy and paste the API token into the Script Variable value field.
A screenshot of a computer window showing where to create a Script Variable
2. Go to the Listeners page in ScriptRunner and add the Listener script above.

3. Provide the information needed in the following lines:
  • Line 3: Space key
  • Line 4: Page title
  • Line 6: Custom field ID in format customfield_xxxxx
  • Lines 62, 72, and 86: User email and user API token variables
4. Make sure the Listener is configured for 'Page Updated' event and run as 'ScriptRunner Add-on User'.

Want to see this script in action? Check out the demo video below.
Yellow star icon with tick mark

The outcome

With minimal work, your Project Management team can make new course codes immediately available, and you have peace of mind that they're accurately synced for tracking and billing work in Jira. Your teams now spend less time on invoicing-related admin, and with the associated reduction in human error, your client billing revenue has increased. Result!

Need help automating a solution for your business problem?

Set yourself up for success—chat to our customer success team today.

Published on 15 July 2025
Authors
A photo of Max Lim
Max Lim
Headshot of Alastair Wilkinson with pink background
Alastair Wilkinson
Share this tutorial