You can use Groovy to display a "calculated" custom field on an issue.

This is a replacement only for writing a full calculated custom field plugin. You cannot edit the value, or update the issue. The value for a field is calculated at the time an issue is displayed or updated (in exactly the same way as a custom field plugin). You could use it for example to pull in data from an external system, or to show some value that’s calculated from the values of the other issue fields, or even other issues etc.

It’s also possible to search on these values if you set up one of the four shipped searchers. If you do, the value is calculated and stored in the index at the time that the issue is indexed, eg when it is modified or transitioned, or when you do a full reindex.

You cannot make any modifications within a calculated field…​ the only thing you can do is pull information from the current issue, or other issues, or outside of the JIRA environment altogether, and display it.

More Advanced Notes on Caching

It’s worth expanding a little on what I said above. You can only reliably use a calculated field to display a value that is based on the issue’s fields, or subtasks' fields, or linked issues. The reason why is because the value in the Lucene index is only updated when the issue is updated. If you compute some value based on data outside of the current issue, the value in the index may differ from the value displayed on the issue.

JIRA asks the field for the value a ridonkulous amount of times - 8 times just for viewing an issue. This means your script will be executed 8 times. Therefore there is a thread local cache…​ for subsequent requests the computed value is just retrieved from the cache. If the issue has a different last update date the cache is invalidated. As of 2.1.4 (not released at time of writing), the following actions now also invalidate the cache - changes to the script text (for inline scripts) and changes to the last modified date of the script file, if you are using one. If, in previous versions of the plugin, refreshing the page showed a different value, it’s because different threads had different cached values - that should be fixed in 2.1.4.

If your script relies on data from external system you can invalidate the cache altogether, although you should test first, particularly if you are doing things like running complex JQL queries. To disable the cache add the following line to your code:

enableCache = {-> false}

That’s an example for disabling it completely, you can use any other variables to return a Boolean. You may have to do this if you are computing data from linked issues, however the indexed value should always be correct.

What if your field relies on data outside its scope and you want to search on it? You can’t, JIRA doesn’t have this capabilty…​ write your own searcher.

Quick Start for Experimenting

  • Navigate to Admin→Script Fields

  • Click "Add New" and select the "Custom Script Field"

  • Add code…​

  • Select the velocity template to display the field

  • Preview

  • Save

Getting Started

Navigate to Admin → Script Fields. Tip: type gg (or .) then Script in the popup.

This page displays the different field configurations for this field type. It’s possible to have different scripts for different field configs for the same custom field, but don’t do this right now to keep things simple.

Click the "Add New Item" button (see screenshot below) to see a list of built-in script fields available.

csf add new item

Select the "Custom Script Field" option

You can either point to a script accessible to jira using an absolute path, or a path relative to jira’s working directory (preferred), or type your script in inline.

Scripted Field Page

The final part of configuration is to choose which renderer to use. The renderer also determines which issue panel, eg Dates, People, or the default main panel the value will be shown in in the view issue screen. As of 3.0, script fields using either user template can be used in notification schemes, which means you can dynamically generate the recipient of an email, eg based on component lead, priority, etc.

The names are taken from one of the internal jira files so might not seem to make total sense. Use the following table as a guideline for which one to use:

Your script returns…​ Template Issue Panel

A string

Free text field

main

A string as html

HTML

main

A date

Date Time

Dates

A number

Number Field

main

A user

User Picker

People

List of users

Multi User Picker

People

Something else

Custom

main

When getting started use the "free text" template without any indexer.

Obviously all of these three things need to tally:

  • Your return type from the script

  • The indexer if you are using one

  • The template Next, get the key of an existing issue and enter it in the "Preview …​ " box, and press Go. Your script will be compiled and executed using the issue, so you can see how the field will appear on that particular issue.

Depending on the complexity of the script you will need to test with multiple issues and different inputs.

Indexing Free Text Fields

If you use one of the built-in text searchers, you will be able to query on this field. But to also use them in filter statistics gadgets you should make use of the jira natural searchers plugin.

After installing this plugin switch the indexer for the scripted field to the "Exact Text Searcher (natural)", then reindex if necessary.

Tips, Tricks and Gotchas

It’s always safe to return null from a script, regardless of the searcher. If you don’t want the calculated value to appear you should return null.

The number searcher expects a java.lang.Double from the issue, so make sure you cast appropriately if you are using the number searcher. The previewer has some smarts to show you if you have screwed up. In this example I am returning the issue.id which is a Long.

searcherError

I can fix this by using the script: issue.id as Double

The consequences of getting this wrong are relatively severe.

  1. If you do a full reindex the indexer will barf when it gets to this issue. If this happens either fix the script or disable the plugin, and reindex.

  2. You won’t be able to transition this issue anymore.

All this means is you should test well, preferably in a test instance. If you have a complex script add error handling so that any exceptions are caught and logged.

User Fields

If you use the User Picker (single user) or User Picker (multiple users) templates you need to return an ApplicationUser, or List of ApplicationUser in the multiuser case. For Example:

import com.atlassian.jira.user.ApplicationUsers

ApplicationUsers.from(issue.reporter)

Stack Overflows

In a calculated custom field, you cannot under any circumstances try to get the value of the same calculated custom field, otherwise the same script will execute, which will call it again, and so on, leading to a stack overflow error. So you can’t do that, or call any method which will try to evaluate all of the custom fields on the issue. If you do this, you will crash your JIRA.

JQL Searches in Script Fields

Running JQL queries in script fields is generally fine. However, when you are doing a full, stop-the-world reindex, the query results are not necessarily going to be correct, as they may depend on querying on issues that have not yet been indexed.

Firstly, consider whether you need to do a JQL search in a script field, and if you do ensure the field context is not wider than it need be, e.g. only apply to the relevant projects and issue types. The screens are irrelevant when considering a full reindex.

To background reindex all issues containing script fields you can use the following script, which you could either run in the console or attach to the ReindexAllCompletedEvent using a script listener:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.index.IssueIndexingService
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.issue.search.SearchProviderFactory
import com.atlassian.jira.issue.util.IssueIdsIssueIterable
import com.atlassian.jira.jql.builder.JqlQueryBuilder
import com.atlassian.jira.jql.query.IssueIdCollector
import com.atlassian.jira.task.context.LoggingContextSink
import com.atlassian.jira.task.context.PercentageContext
import com.onresolve.scriptrunner.runner.ScriptFieldManagerImpl
import org.apache.log4j.Level

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService)
def searchProviderFactory = ComponentAccessor.getComponent(SearchProviderFactory)
def issueManager = ComponentAccessor.getIssueManager()

/*
 Loop through all script fields and get their contexts
 Create a query and execute it
 Reindex all results
*/

def builder = JqlQueryBuilder.newBuilder().where().defaultOr()

def scriptFields = customFieldManager.getCustomFieldObjects().findAll {
    it.customFieldType.descriptor.completeKey == ScriptFieldManagerImpl.SCRIPT_FIELD_TYPE
}

if (scriptFields.find {
    it.allProjects && it.allIssueTypes
}) {
    log.warn("A full background reindex is required at least one script field has global scope")
}
else {
    scriptFields.each {
        def clauseBuilder = JqlQueryBuilder.newClauseBuilder().defaultAnd()

        if (!it.allProjects) {
            clauseBuilder.project(* it.associatedProjectObjects*.id).buildClause()
        }
        if (!it.allIssueTypes) {
            clauseBuilder.issueType(* it.associatedIssueTypeObjects*.id).buildClause()
        }

        builder.addClause(clauseBuilder.buildClause())
    }
}

def query = builder.buildQuery()

def issueIdCollector = new IssueIdCollector(searchProviderFactory.getSearcher(SearchProviderFactory.ISSUE_INDEX).getIndexReader())
searchProvider.search(query, user, issueIdCollector)

def total = issueIdCollector.issueIds.size()
def issuesIdsIterable = new IssueIdsIssueIterable(issueIdCollector.issueIds*.toLong(), issueManager)
def loggingContextSink = new LoggingContextSink(log, "Indexing script field issues {0}% ($total total)", Level.DEBUG)
issueIndexingService.reIndexIssues(issuesIdsIterable, new PercentageContext(total, loggingContextSink))
Previous JIRA versions may have thrown a SearchUnavailableException exception, however I was not able to reproduce this in any supported version. It’s possible that this only happens when indexes are corrupt. If this is the case disable ScriptRunner, do a full reindex, enable ScriptRunner, then reindex again.

Scripting

There is not much available in the binding for this one, just:

componentManager

Instance of com.atlassian.jira.ComponentManager This is here for backward compatibility. You should really use the ComponentAccessor instead.

log

logger so you can do eg log.warn("…​")

getCustomFieldValue

closure - a utility method to let you look you get the value of a custom field on this issue, by name or by id (as Long). Eg getCustomFieldValue("mytextfield")

Custom Templates

As of plugin version 2.0.4 you can write your own template. Select custom, and a textbox will drop down for you to enter the template.

Whatever you returned from the script part of the field will be available in $value. You could use a custom template if you want to display the field differently from the way it is stored, or the way one of the default templates displays it. As usual, experiment using the preview facility.

These are the items available in the velocity context:

Displaying Script Fields in Transition Screens

JIRA does not display calculated custom fields on transition screens.

A possibility is to override one of the other custom fields, eg GenericTextCFType, however then you need to match the base class to the type of value that the calculated field produces, for instance a text or number or user type. This massively increases complexity and reduces flexibility.

My preferred solution therefore is to use javascript to do this.

The downside is that as there is no public API for the JIRA frontend, so it’s possible that presentation issues will appear from one version to the next.

To make your calculated field appear on transition screens, paste the following javascript into the description (Admin → Field Configurations) of any field that appears on the transition screen(s) you care about, eg Resolution, NOT the calculated field itself. You only need to modify the two values in the mandatory config section.

<script type="text/javascript">
(function ($) {

    // ---------------------------------- MANDATORY CONFIG ----------------------------------
    var fieldName = "Scripted Field" // display name - does not have to match the name of the field
    var fieldId = "customfield_14013" // field Id
    // ---------------------------------- END MANDATORY CONFIG ------------------------------

    function addCalculatedField(e, context) {
        var $context = $(context);

        // if you want you can limit this to certain actions by checking to see if this value is in a list of action IDs
        if (!$("input[name='action']").val()) {
            return;
        }

        // multiple handlers can be added if you do an action, then cancel repeatedly
        if ($context.find("#scriptedfield_" + fieldId).length > 0) {
            return;
        }

        var issueKey = $("meta[name='ajs-issue-key']").attr("content");
        if (!issueKey) {
            issueKey = $("#key-val").attr("rel"); // transition screens in full page mode
        }

        var paddingTop = AJS.$("meta[name='ajs-build-number']").attr("content") < 6000 ? "1" : "5";

        var fieldGroupHtml = '<div class="field-group">' +
            '<label for="' + fieldId + '">' + fieldName + '</label>' +
            '<div style="padding-top: ' + paddingTop + 'px" id="scriptedfield_' + fieldId + '"> ' +
            '<span class="aui-icon aui-icon-wait">Loading, please wait</span></div>' +
            '</div> ';

        // Modify this select if you want to change the positioning of the displayed field
        $context.find("div.field-group:first").before(fieldGroupHtml);

        $.ajax({
            type: "GET",
            "contentType": "application/json",
            url: AJS.params.baseURL + "/rest/api/2/issue/" + issueKey + "?expand=renderedFields&fields=" + fieldId,
            success: function (data) {
                if ("fields" in data && fieldId in data.fields) {
                    var fieldValue = data.fields[fieldId];
                    $context.find("#scriptedfield_" + fieldId).empty().append(fieldValue);
                }
                else {
                    $context.find("#scriptedfield_" + fieldId).empty().append("ERROR - bad field ID");
                }
            },
            error: function () {
                $context.find("#scriptedfield_" + fieldId).empty().append("ERROR");
            }
        });
    }

    JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
        addCalculatedField(e, context);
    });

})(AJS.$);
</script>

Note that the plain value is returned, the renderer is not respected. If you know how to get the rendered value let me know. The REST API does not seem to provide this for calculated fields even with expand=renderedFields.

I’ll consider incorporating this into the plugin with a proper UI if there is enough interest.

Examples

See script field examples in the recipes section.

Calculate a number based on other fields

Eg let’s say we have a number field called Severity, and we want some new value that is the product of the priority and severity:

def severity = getCustomFieldValue("Severity")
if (severity) {
    return severity * Integer.parseInt(issue.priority.id)
}
else {
    return null
}

Note that I’m careful to test this against an issue that has no Severity value set.

Total time this issue has been In Progress

This will show the duration that an issue has been in the In Progress state - summing up multiple times if necessary.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.history.ChangeItemBean

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()

def inProgressName = "In Progress"

List<Long> rt = [0L]
def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
    def timeDiff = System.currentTimeMillis() - item.created.getTime()
    if (item.fromString == inProgressName) {
        rt << -timeDiff
    }
    if (item.toString == inProgressName){
        rt << timeDiff
    }
}

def total = rt.sum() as Long
return (total / 1000) as long ?: 0L

Use the Duration Searcher. For the template choose Duration.

Number of attachments an issue has

This example is superceded by the JQL function hasAttachments.

A simple scripted field to show the number of attachments. This could easily be modified to show the number with a specific extension etc.

import com.atlassian.jira.component.ComponentAccessor

def attachmentManager = ComponentAccessor.getAttachmentManager()
def numberAttachments = attachmentManager.getAttachments(issue).size()

// use the following instead for number of PDFs
//def numberAttachments = attachmentManager.getAttachments(issue).findAll {a ->
//    a.filename.toLowerCase().endsWith(".pdf")
//}.size()

return numberAttachments ? numberAttachments as Double : null

Use the Number Searcher with the Number template.

Further Examples

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.