This script fetches all of the projects in a Jira Cloud instance. It then fetches all of the project roles for that project, and finally fetches all of the users in that role for that project. In this way, it iterates through the projects and returns information about the users in the project roles.


 import groovy.json.JsonSlurper

def sb = []
//Define a string buffer to hold the results

def getUsers = get("/rest/api/2/project")
  .header('Content-Type', 'application/json')
//Get the list of projects in the instance

def content =
//Get the raw body contents of the HTTP response

def scanner = new java.util.Scanner(content).useDelimiter("\\A")
String rawBody = scanner.hasNext() ? : ""
def json = new JsonSlurper().parseText(rawBody)
//Turn the raw body contents into JSON

json.each{ project ->
//Iterate through the projects

  def getRoles = get("/rest/api/2/project/$")
    .header('Content-Type', 'application/json')
//For each project, get the list of roles

  getRoles.body.each{ projectRole ->
  //Iterate through the project roles

      def getRoleMembers = get("$projectRole.value")
      .header('Content-Type', 'application/json')
      //Return the details about each role

    getRoleMembers.body.actors.each{ roleMember ->
    //Get all the actors (users) in that role

        sb.add("$   $roleMember.displayName")

return sb
//Return the results


Mitigating CORS Errors With Custom Jira REST API Endpoints

If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing.  Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain.  CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.

Let’s talk about the example that I encountered.  I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro.  However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself.   Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.

We can address this by creating a custom REST API endpoint in Confluence (or Jira).   In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.

In other words, the custom REST API endpoint acts


Tempo Planner allows for planning team capacity and schedules within Jira.  However, you may have some need to pull that resource planning information out of the Tempo interface and add it to a ticket.

The Tempo API has some severe limitations, but where there’s a will there’s a way.

Team Info

The first thing we’ll examine is how to get information on all of the teams in Tempo Planner.  According the documentation, this isn’t possible.  Per the API documentation, you can return limited very information about plans and allocations. 

Naturally I found this to be unacceptable, and I figured out a way to have the API return all of the teams.   One of the undocumented API endpoints is a search function: /rest/tempo-teams/3/search.    One of the tricks to using this method is that it’s not a GET, it’s a POST, so we have to supply a search parameter as a payload.  When we POST to this endpoint, we supply some JSON: {“teamSearchString”:”<string>”}.  But here’s the rub: the API will accept an empty search string, and return all of the teams as a result.

Allocation Info

Much like team info, there is no public Tempo API endpoint that will

There may come a day when you’re asked to create a large number of Confluence pages. Rather than doing it by hand, why not script it?

This Python script essentially does two things: it reads the CSV file, and it sends page creation requests to a Confluence server.   

For each row in the CSV file, it assumes the page name should be the value in the first cell of the row.  It then generates an HTML table that is sent as part of the page creation request. 

Rather than generating HTML, this could be useful for setting up a large number of template pages, to be filled in by various departments.  It could also run as a job, and automatically create a certain selection of pages every week or month, to store meeting notes or reports.

Please note that in order to connect to the Confluence server, you’ll need to generate a Personal Access Token.


 import csv
import requests
import json
import html
import logging

# Initialize logging

api_url = 'https://<url>.com/rest/api/content/'
#What's the URL to your Confluence DC instance?

file_path = "<your CSV file path>"
#where is the file stored locally?

parent_page_id = "<your parent page ID>"

The amount of code required to fetch information from Confluence Cloud and bring it into Jira Cloud is a bit shocking. In a good way.

Here’s the code:

 import org.jsoup.*

def authString = "<authstring>"

def fieldConfigsResult = get("https://<url>")
  .header('Content-Type', 'application/json')
  .header("Authorization", "Basic ${authString}")

def storage =

return storage


In the end it’s all just REST.  So long as you can authenticate, UNIREST allows us to pretty easily fetch information from other sites.

If you’d like to learn more about authenticating against Jira Cloud, check out my post on the subject.

The request on the Atlassian Forums that caught my eye last night was a request to return all Jira Cloud attachments with a certain extension.   Ordinarily it would be easy enough to cite ScriptRunner as the solution to this, but the user included data residency concerns in his initial post.

My solution to this was to write him a Python script to provide simple reporting on issues with certain extension types.    Most anything that the rest API can accomplish can be accomplished with Python; you don’t HAVE to have ScriptRunner.

The hardest part of working with external scripts is figuring out the authorization scheme. Once you’ve got that figured out, the rest is just the same REST API interaction that you’d get with UniREST and ScriptRunner for cloud.

Authorizing the Script

First, read the instructions: 


1. Generate an API token:

2. Go to (or figure out the Python module to do the encoding)

3. Base-64 encode a string in exactly this format: youratlassianlogin:APIToken.   
If your email is and the API token you generated is:



Then the string you base-64 encode is:


Do not forget the colon between the two pieces.