Last updated Mar 28, 2024

Jira expressions

Introduction

Jira expressions is a domain-specific language designed with Jira in mind, evaluated on the Jira Cloud side. It can be used to evaluate custom code in the context of Jira entities.

Several Jira REST API operations, Forge modules, and Connect modules make use of Jira expressions.

The REST API operation to evaluate expressions can be used to test expressions. The operation can also be a useful way to retrieve data. For example, you can build an efficient and lightweight view of issue comments by fetching a small set of data (ID, author, and the content excerpt) with this expression:

1
2
issue.comments.map(c => { 
    id: c.id, 
    author: c.author.displayName, 
    excerpt: c.body.plainText.slice(0, 200) + '...'
})

The web condition for Connect modules provides extensive control over the visibility of web elements. For example, use the following expression to show your web panel only if the current user commented on the issue:

1
2
issue.comments
     .filter(c => c.author.accountId == user.accountId)
     .length > 0

Syntax and semantics

Jira expressions use JavaScript-like syntax. They support a subset of JavaScript constructs and features, here is a brief overview of some of the main ones:

Static and computed member access

Use static member access to access an object's field when the field's name is known. For example, issue.key is an expression that accesses the key field from issue. Use computed member access to dynamically create the name of the field to access or where the field name contains special characters and cannot be accessed using static member access because of the expression syntax.

Using computed member access is useful when obtaining entity properties, which usually contain dots or dashes in their names. For example:

1
2
issue.properties['com.your.app.property-name']

Optional chaining

Accessing properties in a Jira expression may fail, for example, where:

  • the left-hand side of the operation is null. For example, in the expression a.b where the value of a is null.
  • the property does not exist.

In expressions where such strict rules are not desired, use the optional chaining operator ?.. This operator behaves in the same way as regular member access, but with one crucial difference: when accessing the property fails, null is returned.

Examples:

  • issue.properties?.myProperty?.a?.b—this expression returns null if there is no myProperty defined in the issue, or if there is no a.b path in the value of the property.
  • issue?.customfield_10010—this expression returns null if the custom field doesn't exist.

The operator can also be used in combination with computed member access, for example: issue?.[fieldName].

Indexed access

Members of lists are accessed by index.

For example, to get the first issue comment, write:

1
2
issue.comments[0]

Mathematical operators

Jira expressions support the addition (+), subtraction (-), multiply (*), divide (/), and modulo (%) mathematical operations.

For example, to check if the number of comments on an issue is even, write:

1
2
issue.comments.length % 2 == 0

Boolean operators

The JavaScript logical operators—conjunction (&&), disjunction (||), and negation (!)—are supported. If used with boolean values (true or false), the logical operators follow the rules of classical boolean algebra. The operators can be used with any other type and then follow the JavaScript semantics. Use of the logic operators with non-Boolean values is useful when defining default values.

For example, to get the value of the issue property "myProperty" or use a default value when "myProperty” is not defined, write:

1
2
issue.properties["myProperty"] || "default value"

The preceding expression may behave unexpectedly, as some non-null values are treated as false (such as the number 0 or an empty list). To define a default value only when the property value is null, use the nullish coalescing operator (??), for example:

1
2
issue.properties["myProperty"] ?? "default value"

Comparisons

Values can be compared. The comparison operators available depend on the type of the values. For example, it's possible to check if one Number is less than or greater than another number, but Lists or Strings can be tested only for equality. Only values of the same type can be compared.

For example, to check if an issue has more than 0 comments, write:

1
2
issue.comments.length > 0

Variable assignment

A Jira expression can be defined as a series of variable assignments ending with an expression, each separated by a semicolon. For example, this expression returns a String containing the number of comments on an issue:

1
2
  let issueKey = issue.key;
  let commentsLength = issue.comments.length;
  `Issue ${issueKey} has ${commentsLength} comments.`

The let keyword is optional. For example, line 1 of the example can be written as issueKey = issue.key;.

Arrow functions

Jira expressions do not support classical imperative loops. Instead, they follow the modern paradigm of functional programming and provide a set of list processing methods, along with the syntax for arrow functions, also knows as lambdas. These functions are written in the form of x => y, where x is the name of the variable that can be used in the function's body, denoted here as y. For example, to return the number of comments with contents longer than 100 characters, first map the comments to their texts, then filter them to leave only those long enough:

1
2
issue.comments
     .map(c => c.body.plainText)
     .filter(text => text.length > 100)
     .length

The function body can take the form of a code block with variable assignments. For example, this expression calculates the percentage of comments that were authored by the current user for every issue:

1
2
issue.map(i => {
   let myComments = issue.comments.filter(c => c.author == user);
   let otherComments = issue.comments.filter(c => !myComments.includes(c));
   return myComments.length / (myComments.length + otherComments.length);
});

List literals

Lists can be obtained from context objects. They can also be created manually. For example, to check if an issue's type is Bug or Task, create a list with the two types and use the includes() method to test if the issue's type value is one of the two listed:

1
2
['Bug', 'Task'].includes(issue.issueType.name)

Object literals

Jira expressions can return structured data with the use of object literals. For example, to return an object containing only the authors and contents from every comment, not the entire comment, write:

1
2
issue.comments.map(c => { 
    author: c.author, 
    body: c.body.plainText 
})

Conditional expressions

Conditional expressions are used when different results should be returned depending on whether a condition is true or false.

For example, to check whether there are any comments on an issue before getting the first comment's author, write:

1
2
issue.comments.length > 0 ? issue.comments[0].author : null

Conditional statements

Where conditional expressions would be too cumbersome to write or read, you can use fully-fledged conditional statements.

For example, here's a simple implementation of the classic FizzBuzz problem using conditional statements:

1
2
numbers.map(i => {
    if (i % 3 == 0 && i % 5 == 0) {
        return "FizzBuzz";
    } else if (i % 3 == 0) {
        return "Fizz";
    } else if (i % 5 == 0) {
        return "Buzz";
    } else {
        return `${i}`;
    }
})

Note that, unlike in many popular programming languages, the final else is always required, since the if statement is treated as an expression and must always yield a value.

Runtime error handling

Expressions may fail at runtime when an operation they're trying to perform can't be completed in any reasonable way. Two examples of such operations are accessing a property of null or adding a number to an object. Errors like these can be caught and reacted upon by using the try-catch syntax.

For example, the following expression returns the details of an error if one occurs:

1
2
try {
  <expression that may fail>
} catch (e) {
  return `${e.location} -- ${e.message}` 
}

See the Error type for a detailed list of properties available for caught errors.

It's also possible to fail the entire expression manually with the throw keyword. You can use it with your own error message (for example, throw 'error message') as well as with errors caught with a try-catch statement. It may be particularly useful when you want to recover from some errors, but not others, like this:

1
2
try {
  <expression that may fail>
} catch (e) {
  if (<does it make sense to return anything?>) {
    return <some default value>;
  } else {
    throw e; // rethrow the original error and fail the entire expression
  }
}

Examples

The following examples demonstrate how to write Jira expressions and what they can do.

Get contents of all comments added by the current user in the current issue:

1
2
issue.comments
     .filter(c => c.author.accountId == user.accountId)
     .map(c => c.body)

Check if the current user is the one stored in a project's entity property:

1
2
user.accountId == project.properties['special-user'].accountId

Check if the issue type is either a Bug or Task (using a regular expression):

1
2
issue.issueType.name.match('^(Bug|Task)$') != null

Retrieve IDs of all linked issues along with the link name:

1
2
issue.links.map(link => { 
    name: link.type[link.direction], 
    issue: link.linkedIssue.id 
}) 

Data aggregation

To aggregate data, use the set() method for Map and the reduce() method for List.

This is particularly useful in combination with a JQL query that can be provided in the REST API to load a list of issues into the expression context.

For example, the following expression will count issues by their status name:

1
2
issues.reduce((result, issue) => 
            result.set(
                issue.status.name, 
                (result[issue.status.name] || 0) + 1), 
            new Map())

Note that map[issue.status.name] || 0 will return either the current mapping for the given status, or 0 if there is no mapping yet. This is a handy way to declare default values.

If the above expression is evaluated using the REST API, the result will be, for example:

1
2
{
    "value": {
        "To Do": 2,
        "In Progress": 10,
        "Done": 5
    }
}

Context variables

Depending on the context in which a Jira expression is evaluated, different context variables may be available:

  • user (User): The current user. Equal to null if the request is anonymous.
  • app (App): The Connect or Forge app that made the request or provided the module. Always available for expressions used in app modules and REST API requests made by apps. Requests made by Forge apps must be authenticated with asApp().
  • issue (Issue): The current issue.
  • issues (List<Issue>): The list of issues available when a JQL query is specified in the request context when using the REST API.
  • project (Project): The current project.
  • sprint (Sprint): The current sprint.
  • board (Board): The current board.
  • serviceDesk (ServiceDesk): The current service desk.
  • customerRequest (CustomerRequest): The current customer request.

These variables are registered in the global scope. For example, to check if the request is not anonymous, write: user != null.

To check if a context variable is available, use the typeof operator. It will return "undefined" for unavailable variables. For example, to check if the current project is next-gen, but only if the expression is evaluated in the context of a project, write:

1
2
typeof project == 'undefined' || project.style == 'next-gen'

The above will return true in any of these occur:

  • the expression is not evaluated in the context of a project (the project variable is not available).
  • the expression is evaluated in the context of a project and the project is next-gen.

Loading data

There are two ways to interact with Jira entities:

  • Use context variables, as described above.
  • Load objects (like issues, projects or users) at runtime using their respective ID or key.

Loading an object is done with type constructors. For example, to get a summary of an issue with key HSP-1, write:

1
2
new Issue('HSP-1').summary

The method above loads the issue from the database using the given key, then gets the issue’s summary. Note that null is returned by the constructor if the issue doesn't exist or you don't have permission to view it.

Consult the type reference to learn which types have similar constructors.

Keep in mind that loading data this way is considered an expensive operation, and there is a limit of how many expensive operations a single expression evaluation can have. Because of this, only load data where it’s not possible to use context variables.

Entity properties

Using Jira expressions, it is possible to access entity properties of any entity that supports them, that is: issue, project, issue type, comment, user, board, sprint. App properties are also available. To do this, get the properties field of the appropriate object. For example, app.properties. The field returns what can be thought of as a map of all properties, indexed by their keys.

Read more about how to interact with properties in the EntityProperties object documentation.

Working with JSON

The following functions are available to convert between different representations of JSON values:

  • JSON.stringify(Any) - converts a value into a JSON string. Only values that consist of the following types are supported: String, Number, Boolean, List, Map.
  • JSON.parse(String) - the inversion of JSON.stringify. Parses a JSON string into an equivalent value.

The JSON.stringify function can be particularly useful when working with issue custom fields, which are always returned as JSON values in the same format as in the Get issue REST API.

For example, if you are only interested whether the value of a custom field contains a certain string, you don't need to manually inspect the entire list or map. Instead, convert the whole value into a string with JSON.stringify and search for the substring within:

1
2
JSON.stringify(issue.customfield_10000).includes('My searched string')

Date and time

Fields that contain timestamps, such as issue.created or issue.resolutionDate, are returned as objects of the Date type, which is based on the JavaScript Date API.

Fields that contain dates only, such as issue.dueDate, are returned as the timezone-agnostic CalendarDate type, which is like Date, but with a limited set of methods (methods related to time or timezones are not available).

A Date or CalendarDate object can be transformed into three different String formats:

  • ISO format. For example, 2018-06-29T12:16:37.471Z (Date format) or 2018-06-29 (CalendarDate format). To transform a date to this format, call the toISOString() method. This will return a string in the ISO 8601 extended format. For example, issue.created.toISOString().
  • Jira REST API format. For example, 2018-06-29T22:16:37.471+1000 (Date format) or 2018-06-29 (CalendarDate format). Returning dates from Jira expressions renders them in the Jira REST API format. For example, issue.created.
  • Human-readable format. For example, 29/Jun/18 10:16 PM (Date format) or 29/Jun/18 (CalendarDate format). To transform a date to this format, call the toString() method. This will return a string in the human-readable format, according to the current user's locale and timezone. For example, issue.created.toString(). The same format is also used if a date is concatenated with a string. For example, 'Due date: ' + issue.dueDate.

A Date object can also be converted to a CalendarDate object by using either toCalendarDate() or toCalendarDateUTC(). These methods remove the time information from the object, leaving only the calendar date, in the current user's timezone or the UTC timezone, respectively.

Date objects of the same type can be compared using regular comparison operators. For example, to get comments that were added after the issue's due date, write:

1
2
issue.comments
     .filter(c => c.created.toCalendarDate() > issue.dueDate)

A date can be modified by adding or subtracting units of time. To do this, use the methods below. Each of these methods take a date and a number of units of time, then create a new modified date.

  • date.plusMonths(Number): Creates a new date that is the original date plus the specified number of months.
  • date.plusDays(Number): Creates a new date that is the original date plus the specified number of days.
  • date.plusHours(Number): Creates a new date that is the original date plus the specified number of hours.
  • date.plusMinutes(Number): Creates a new date that is the original date plus the specified number of minutes.

(All methods above have a subtraction counterpart. For example, date.minusMonths(Number).)

Date modification methods can be used to build expressions that assert when Jira events have occurred. To do this, get the current date and modify it, then compare the modified date to the date of the event. Here's an example of how to check if an issue has been updated in the last three days:

  1. Get the current date. To do this, create a new Date object (that is, new Date()).
  2. Modify the date, as desired. For example, new Date().minusDays(3) is the current date minus three days.
  3. Compare the modified date to the date of the issue.updated event. For example, issue.updated > new Date().minusDays(3) will return true if the issue has been updated in the last three days.

Monitoring

Jira expressions can be specified in several modules in the app descriptor (for example, web conditions). The execution of these expressions is performed on the Jira side, with no direct interaction with the app. For monitoring and testing purposes, however, it’s useful for apps to know when Jira fails to evaluate their expressions, be it due to invalid syntax, semantic bugs caused by unexpected data, or even a temporary glitch on the Jira side.

To make this possible, Jira allows apps to declare a webhook to be notified whenever such failures occur. For example, you can declare the webhook in your descriptor as follows:

1
2
{
    "webhooks": [
      {
        "event": "jira_expression_evaluation_failed",
        "url": "/jira-expressions-monitoring"
      }
    ]
}

You will start receiving POST callbacks to /jira-expressions-monitoring whenever any of the expressions provided by your app fails. Expressions from any of the following modules will be included:

Expressions declared by other apps, or expressions evaluated using the REST API are excluded from this mechanism.

Here is an example payload sent in the webhook callback:

1
2
{
  "timestamp": 1562571661978,
  "webhookEvent": "jira_expression_evaluation_failed",
  "expression": "'1' == 1",
  "errorMessages": ["Evaluation failed: \"1 == '1'\" - operator \"==\" is not applicable to types: Number and String"],
  "moduleKey": "module-that-provided-the-expression",
  "validator.id": "bb98deec-4f14-4431-9dbd-fc591cb71f34",
  "workflow.name": "HSP: Project Management Workflow",
  "context": {
    "issue": {
      "id": "10000",
      "key": "HSP-1"
    },
    "project": {
      "id": "10000",
      "key": "HSP"
    },
    "transition": {
      "id": 21,
      "name": "Done",
      "from": {
        "name": "To Do",
        "id": "10000"
      },
      "to": {
        "name": "Done",
        "id": "10001"
      }
    }
  }
}

The JSON payload always contains the following properties:

  • timestamp: The time when the expression failed to evaluate, in epoch milliseconds.
  • webbookEvent: The name of the event. In this case it will always be jira_expression_evaluation_failed.
  • expression: The expression that failed to evaluate correctly.
  • errorMessages: A list of error messages that explain why the evaluation failed. The messages are the same as those returned by the REST API.
  • context: A set of context variables available to the expression during its evaluation. Note that in the example above, some properties of variable values were omitted to keep it succinct.

Additionally, these properties may be included if applicable, depending on where the expression comes from:

Restrictions

Some restrictions apply to the evaluation of expressions. While the limits are high enough not to interfere with any intended usage, it's important to realize that they do exist.

Expensive operations

The expression can execute at most 10 expensive operations. Expensive operations are those that load additional data, such as entity properties, comments, or custom fields.

Most issue fields are optimized to execute just one expensive operation even if the expression is processing large data sets. However, it may happen that expression's complexity depends on the data size. If that's the case, we encourage you to create a feature request in the ACJIRA project, to allow such expressions to be processed with constant complexity.

Operations on unbounded collections (for example, issue comments) may appear to compute in constant time, but they are limited to processing up to 1,000 items. For example, issue.comments.map(c => c.properties.myProperty) computes in constant time for the first 1,000 comments on the issue, but consumes one expensive operation for each comment after that. To make sure your expression evaluates successfully, always limit the data set that is being processed, for example, by using the slice method: issue.comments.slice(0, 1000).map(c => c.properties.myProperty).

In the Type reference, we marked all properties, functions, and constructors that trigger expensive operations with the following labels to make it easier for you to determine the final complexity of your expressions:

  • 1: these always count as one expensive operation, even if applied to a list.
  • N: these count as one expensive operation whenever accessed.
  • len 1: these count as one expensive operation if used in combination with the length property.

For example, the parent property of the Issue type is marked with 1. This means that you can safely retrieve parents even for a list of issues. The expression issues.map(issue => issue.parent) will have the complexity of 1, regardless of the number of issues on the issues list.

However, the comments property is marked with N. This means that while issue.comments will have a complexity of 1, the expression for retrieving comments for a set of issues, issues.map(issue => issue.comment), will quickly reach the limit. This is because the complexity of such an expression depends on the size of the issues list and is equal to N, where N is the size of the list.

On the other hand, comments is also annotated with len 1. This means that while inspecting all comments for a set of issues won't work for more than 10 issues, retrieving the size of the comment list always costs 1, so this expression always has the complexity of 1, regardless of the number of issues on the list: issues.map(issue => issue.comment.length).

REST API

The max number of results returned by the Evaluate Jira expression REST API operation is 10,100 primitive values or 1,000 Jira REST API objects.

Other

Expressions may execute up to 50,000 steps, where a step is a high-level operation performed by the expression. A step is an operation such as arithmetic, accessing a property, accessing a context variable, or calling a function. In most practical cases, a Jira expression is unlikely to exceed the step limit.

The expression's size is limited to 10,000 characters.

Analysing expressions

Use the Analyse Jira expression REST API operation to statically check the characteristics of your expression without evaluating it.

For example, to analyse an expression that fetches the number of comments and attachments for a list of issues:

1
2
issues.map(issue => { 
    key: issue.key, 
    comments: issue.comments, 
    attachments: issue.attachments.length 
})

Send the following request:

POST https://your-domain.atlassian.net/rest/api/2/expression/analyse?check=complexity

1
2
{
  "expressions": [
    "issues.map(issue => { key: issue.key, comments: issue.comments, attachments: issue.attachments.length })"
  ]
}

Which results in the following response:

1
2
{
    "results": [
        {
            "expression": "issues.map(issue => { key: issue.key, comments: issue.comments, attachments: issue.attachments.length })",
            "valid": true,
            "type": "List<{attachments: Number, comments: List<Comment>, key: String}>",
            "complexity": {
                "expensiveOperations": "N + 1",
                "variables": {
                    "N": "issues"
                }
            }
        }
    ]
}

This response shows that:

  • The expression's type is List<{attachments: Number, comments: List<Comment>, key: String}>, that is it returns a List where each item is a Map with these properties:
  • the projected complexity in terms of expensive operations is N + 1, where N is the length of the issues list. This complexity means the expression will fail if the number of issues is greater than 9. This is caused by accessing the “heavy-weight” comments property.

You can also execute expressions with the Evaluate Jira expression REST API operation and use the meta.complexity expand parameter to see the runtime complexity.

Additional Resources

Rate this page: