Custom JQL functions

The JQL function module (for Forge or Connect) creates a new custom JQL function, which appears built-in from the user’s perspective - it’s visible in the editor and shows up in the autocomplete dropdown. However, the function’s business logic is executed within the app codebase. Therefore, it’s important to have a basic understanding of how custom JQL functions work in Jira.

High-level overview

  1. Whenever a JQL query with custom functions is sent for evaluation, the first step is to replace them with native JQL.
  2. Function clause is extracted from the query and the business logic of the app is invoked to execute business logic of the function.
    • App returns a JQL fragment as a result of the function processing.
    • Function clause in original query is replaced by JQL fragment returned by the app.
  3. The query is sent to the JQL engine to be evaluated.

JQL functions processing

Precomputations

Calling an app to evaluate a custom function may be slow. Therefore, instead of doing it on every call, we only evaluate each custom function once and save the result to the database. A stored mapping between a custom function and native JQL is called a precomputation.

Note that whenever we talk about storing precomputations for functions, we actually mean storing them for functions together with their arguments. One function can have multiple precomputations, one for each set of arguments the function was called with.

App’s responsibilities

App must define the business logic for the custom function that will be invoked by Jira. The custom function should return a valid JQL fragment that can later replace the function clause in the original query.

Your app should keep precomputations up to date. For this purpose, we're exposing the new Precomputation API to browse stored precomputations and update them if needed. Each precomputation will contain metadata, such as the last time it was used, which you can use to guide update logic.

Precomputations for functions that weren't evaluated in any JQL query or filter for the last 7 days are completely removed from the database. Thanks to this, apps don't need to keep updating unused precomputations indefinitely, lessening the load both on apps and Jira. Once a function that had its precomputation deleted is evaluated again, the app will be contacted to compute a fresh result and a new precomputation will be stored.

When to update precomputations?

Precomputations and users

Precomputations are not scoped to users, only to the function and its argument set. This reduces the load on both the app and Jira, as the app doesn't need to keep track of precomputations for each user separately.

Note that the final result of a query will still be affected by permissions. A user will never receive issues they don't have permission to see. This means that you don't need to worry about permissions if your app returns a set of issues; just return all issues that satisfy the function clause, and let Jira apply user permissions to the final result.

If all else fails and you need to differentiate the behaviour of your function based on the user, one option is to add a dedicated user ID argument, for example:

1
2
issue in issuesImportantTo("user-id")

Function evaluation

The app will be called whenever the function needs to be evaluated. It's expected to return a JQL fragment that the function clause will be replaced with.

Input

The argument that functionKey will receive looks like this:

1
2
{
  "precomputationId": "<uuid>"
  "clause": {
    "field": "key",
    "type": [
      "issue"
    ],
    "operator": "in",
    "functionName": "issuesWithText",
    "arguments": [
      "Test"
    ]
  }
}

The above input may correspond to the following function clause: issue in issuesWithText("Test").

field is the canonical name of the field in the left-hand side of the function clause. Notice that its value in the example is key, and not issue. This is because some fields can have alternative names in JQL. This particular field can be referred to as key, issue, or issuekey, but the canonical name is key, and this is what's sent to the app, no matter which of the alternatives was used in the original query.

type is a list of value types that the field expects in the right-hand side of the function clause. All fields in JQL have a specific type, and different fields can share the same type. For example, type user is shared by fields like assignee, reporter, or creator, among others.

Functions are scoped to types, not fields, so they're applicable to all fields that share a type. If you want your function to work only with specific fields, you can always return an appropriate error during evaluation when your function receives a request for a field that it doesn't support.

precomputationId is the ID of the precomputation that will be created for the function after its first evaluation finishes. The app can save it for later use, for example to update the precomputation when the function results change.

Output

The response that the function is supposed to return can be either

  • an error message, or
1
2
{
  "error": "Error message returned by app."
}
  • a JSON with a JQL fragment that the function clause will be replaced with in the original query.
1
2
{
  "jql": "id in (1, 2, 3)"
}

Notice that the JQL fragment returned by the app isn't a complete JQL query. It’s just a fragment that'll be inserted into the original query in place of the function.

Our example function clause issue in issuesWithText("Test") could be a part of a longer query, for instance:

1
2
issue in issuesWithText("Test") and assignee is not empty order by created

The final query that will be sent to the JQL engine for final evaluation is equal to the original query, with the whole function clause replaced by the fragment the app returned:

1
2
id in (1, 2, 3) and assignee is not empty order by created

To improve the performance of functions that return a list of issues, enumerate their IDs in your JQL query using the id field: id in (1, 2, 3...). While using issue keys may feel more natural, we recommend enumerating by IDs because our internal systems are optimised for it.

There is a limit of 1,000 right-hand side values in the JQL fragment returned by custom functions.

Name collisions

If your app defines a function whose name matches that of a built-in JQL function, it won’t work.

Also keep in mind that during JQL search processing, built-in functions always have higher priority than app-defined ones.

If multiple apps define a JQL function with the same name, the one invoked during query processing is picked randomly.

Example apps

Here are a few example Forge and Connect apps to help you learn about the JQL function module:

Rate this page: