Last updatedSep 9, 2019

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.

There are currently four ways to use Jira expressions:

The REST endpoint can be used to test the expressions that you plan to use elsewhere, or to load data in a flexible way. For example, if you would like to build an efficient and lightweight visualization of issue comments, you could fetch a minimal required set of data (id, author and the content excerpt) with the following expression:

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

The web condition gives you almost infinite control over the visibility of your web elements. For example, you could use the following expression to show your panel only if the current user commented on the current issue:

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

Syntax and semantics

Jira expressions follow JavaScript syntax. You can think of them as a JavaScript dialect.

The following constructs are supported (this list may not be exhaustive):

  • Static and computed member access Static member access is used when you want to access an object's field and know the field's name at the time of writing the expression. For example, issue.key is an expression that accesses the key field from issue. Computed member access is used when you want to dynamically create the name of the field you are accessing, or if the name contains special characters, in which case accessing the field using the static member access will not be allowed by the syntax. It is especially useful when accessing entity properties, which usually contain dots or dashes in their names. For example, issue.properties['com.your.app.property-name'].

  • Indexed access Individual members of lists can be accessed by index. For example, to get the first issue comment, write: issue.comments[0].

  • Mathematical operators Jira expressions allow all the usual kinds of mathematical operations. You can add, subtract, multiply, or divide numbers. For example, to check if the number of comments on an issue is even, write: issue.comments.length % 2 == 0.

  • Boolean operators The usual logical operators are available: conjunction (&&), disjunction (||) and negation (!). If used with boolean values (true or false), their behavior follows the rules of classical boolean algebra. Each of these operators can also be used with any type, following the JavaScript semantics in this case. The latter is especially useful for defining default values. For example, to get the value of the issue property "myProperty" while also providing a default value in case it's not defined, write: issue.properties["myProperty"] || "default value".

  • Comparisons Values can be compared to each other in different ways, depending on the type. For example, it's possible to check if one number is lesser or greater than another number, but lists or strings can be tested only for equality. Only values of the same type can be compared together. For example, to check if the issue has more than 0 comments, write: issue.comments.length > 0.

  • Conditional expressions Conditional expressions can be used when different results should be returned depending on a condition. For example, to get the first comment's author, we would first need to check if there are comments at all:
1
issue.comments.length > 0 ? issue.comments[0].author : null
  • Arrow functions Jira expressions by design do not support classical imperative loops. Instead, they follow the modern paradigm of functional programming and provide a set of built-in 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
3
4
issue.comments
     .map(c => c.body.plainText)
     .filter(text => text.length > 100)
     .length
  • List literals Lists can not only be obtained from context objects but also created manually. For example, to check if the issue type is either a Bug or Task, create a list with these two types and use the includes() method to test if the actual value is one of the two listed:
1
['Bug', 'Task'].includes(issue.issueType.name)
  • Object literals Jira expressions can return structured pieces of data with the use of object literals. For example, to return only comments' authors and contents instead of the entire comments, create an object containing these two fields for each comment:
1
2
3
4
issue.comments.map(c => { 
    author: c.author, 
    body: c.body.plainText 
})

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
3
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
user.accountId == project.properties['special-user'].accountId

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

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

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

1
2
3
4
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
3
4
5
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
3
4
5
6
7
{
    "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 app that made the request or provided the module. Always available for expressions used in Connect modules, and also in REST API request made by Connect Apps (read more here: Authentication for Connect apps).
  • 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
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 or projects) 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
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.

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
3
4
5
6
7
8
{
    "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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "timestamp": 1562571661978,
  "webbookEvent": "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:

  • 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.
  • The max number of results returned in the response is 10,000 primitive values or 1,000 Jira REST API beans.
  • The expression's length is limited to 1,000 characters or 100 syntactic elements.

You can execute your expression with the REST API and use the meta.complexity expand parameter to see the complexity of the expression and how close it is to reaching the limits.

Additional Resources