Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Creating a Custom JQL Function for Quarters in Jira Data Center Using ScriptRunner

David Shahbazyan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Champions.
June 25, 2026

 

Hi everyone!

I created a custom JQL function in Jira Data Center using ScriptRunner.

The function allows users to search issues by the quarter in which they were created.

Example:

 

issueFunction in createdQuarter(1)

This returns issues created in the first quarter of the current year.

The function uses the current year automatically and converts the selected quarter into the correct created date range.

This function is especially useful for dashboards that show quarterly data. Since it automatically uses the current year, there is no need to manually update the JQL date ranges every year.


Script

package com.onresolve.jira.groovy.jql

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.jql.query.LuceneQueryBuilder
import com.atlassian.jira.jql.query.QueryCreationContext
import com.atlassian.jira.jql.validator.NumberOfArgumentsValidator
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.util.MessageSet
import com.atlassian.query.clause.TerminalClause
import com.atlassian.query.operand.FunctionOperand
import org.apache.lucene.search.Query

import java.time.LocalDate

class CreatedQuarter extends AbstractScriptedJqlFunction implements JqlQueryFunction {

JqlQueryParser queryParser = ComponentAccessor.getComponent(JqlQueryParser)
LuceneQueryBuilder luceneQueryBuilder = ComponentAccessor.getComponent(LuceneQueryBuilder)

@Override
String getDescription() {
"Returns issues created in the selected quarter of the current year. Example: issueFunction in createdQuarter(1)"
}

@Override
List<Map> getArguments() {
[
[
description: "Quarter number: 1, 2, 3, or 4",
optional : false,
]
]
}

@Override
String getFunctionName() {
"createdQuarter"
}

@Override
MessageSet validate(ApplicationUser user, FunctionOperand operand, TerminalClause terminalClause) {
def messageSet = new NumberOfArgumentsValidator(1, 1, getI18n()).validate(operand)

if (messageSet.hasAnyErrors()) {
return messageSet
}

def quarterText = operand.args.first()

if (!(quarterText ==~ /[1-4]/)) {
messageSet.addErrorMessage(
"createdQuarter() accepts only 1, 2, 3, or 4. Example: issueFunction in createdQuarter(1)"
)
return messageSet
}

def query = buildJiraQuery(quarterText as Integer)
searchService.validateQuery(user, query)
}

@Override
Query getQuery(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {
def quarter = operand.args.first() as Integer
def query = buildJiraQuery(quarter)

luceneQueryBuilder.createLuceneQuery(queryCreationContext, query.whereClause)
}

private com.atlassian.query.Query buildJiraQuery(Integer quarter) {
int year = LocalDate.now().year

LocalDate startDate
LocalDate endDate

switch (quarter) {
case 1:
startDate = LocalDate.of(year, 1, 1)
endDate = LocalDate.of(year, 4, 1)
break
case 2:
startDate = LocalDate.of(year, 4, 1)
endDate = LocalDate.of(year, 7, 1)
break
case 3:
startDate = LocalDate.of(year, 7, 1)
endDate = LocalDate.of(year, 10, 1)
break
case 4:
startDate = LocalDate.of(year, 10, 1)
endDate = LocalDate.of(year + 1, 1, 1)
break
default:
throw new IllegalArgumentException("Quarter must be 1, 2, 3, or 4")
}

def jql = "created >= '${startDate}' AND created < '${endDate}'"
queryParser.parseQuery(jql)
}
}

 

1 answer

0 votes
Radwan Almsora
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
June 25, 2026

This corrected ScriptRunner code for Jira Data Center fixes validation errors by implementing the correct JqlFunction interface and properly defining the searchService component to enable filtering by the current year's quarters. The revised script, available below, also updates the date handling to LocalDate.now().getYear() for better compatibility.


package com.onresolve.jira.groovy.jql

 

import com.atlassian.jira.component.ComponentAccessor

import com.atlassian.jira.jql.parser.JqlQueryParser

import com.atlassian.jira.jql.query.LuceneQueryBuilder

import com.atlassian.jira.jql.query.QueryCreationContext

import com.atlassian.jira.jql.validator.NumberOfArgumentsValidator

import com.atlassian.jira.plugin.jql.function.JqlFunction // Fixed import

import com.atlassian.jira.user.ApplicationUser

import com.atlassian.jira.util.MessageSet

import com.atlassian.query.clause.TerminalClause

import com.atlassian.query.operand.FunctionOperand

import org.apache.lucene.search.Query

import java.time.LocalDate

 

class CreatedQuarter extends AbstractScriptedJqlFunction implements JqlFunction { // Fixed interface

    JqlQueryParser queryParser = ComponentAccessor.getComponent(JqlQueryParser)

    LuceneQueryBuilder luceneQueryBuilder = ComponentAccessor.getComponent(LuceneQueryBuilder)

    // Resolved missing searchService reference

    def searchService = ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.search.SearchService)

 

    @Override

    String getDescription() {

        "Returns issues created in the selected quarter of the current year. Example: issueFunction in createdQuarter(1)"

    }

 

    @Override

    List<Map> getArguments() {

        [

            [

                description: "Quarter number: 1, 2, 3, or 4",

                optional : false,

            ]

        ]

    }

 

    @Override

    String getFunctionName() {

        "createdQuarter"

    }

 

    @Override

    MessageSet validate(ApplicationUser user, FunctionOperand operand, TerminalClause terminalClause) {

        def messageSet = new NumberOfArgumentsValidator(1, 1, getI18n()).validate(operand)

        if (messageSet.hasAnyErrors()) {

            return messageSet

        }

 

        def quarterText = operand.args.first()

        if (!(quarterText ==~ /[1-4]/)) {

            messageSet.addErrorMessage(

                "createdQuarter() accepts only 1, 2, 3, or 4. Example: issueFunction in createdQuarter(1)"

            )

            return messageSet

        }

 

        def query = buildJiraQuery(quarterText as Integer)

        searchService.validateQuery(user, query)

    }

 

    @Override

    Query getQuery(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {

        def quarter = operand.args.first() as Integer

        def query = buildJiraQuery(quarter)

        luceneQueryBuilder.createLuceneQuery(queryCreationContext, query.whereClause)

    }

 

    private com.atlassian.query.Query buildJiraQuery(Integer quarter) {

        int year = LocalDate.now().getYear() // Idiomatic Java Time syntax

        LocalDate startDate

        LocalDate endDate

 

        switch (quarter) {

            case 1:

                startDate = LocalDate.of(year, 1, 1)

                endDate = LocalDate.of(year, 4, 1)

                break

            case 2:

                startDate = LocalDate.of(year, 4, 1)

                endDate = LocalDate.of(year, 7, 1)

                break

            case 3:

                startDate = LocalDate.of(year, 7, 1)

                endDate = LocalDate.of(year, 10, 1)

                break

            case 4:

                startDate = LocalDate.of(year, 10, 1)

                endDate = LocalDate.of(year + 1, 1, 1)

                break

            default:

                throw new IllegalArgumentException("Quarter must be 1, 2, 3, or 4")

        }

 

        def jql = "crea

 

ted >= '${startDate}' AND created < '${endDate}'"

        queryParser.parseQuery(jql)

    }

}


 

Suggest an answer

Log in or Sign up to answer