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 2issue.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 2issue.comments .filter(c => c.author.accountId == user.accountId) .length > 0
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:
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 2issue.properties['com.your.app.property-name']
Accessing properties in a Jira expression may fail, for example, where:
null
. For example, in the expression a.b where the value of a
is null
.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]
.
Members of lists are accessed by index.
For example, to get the first issue comment, write:
1 2issue.comments[0]
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 2issue.comments.length % 2 == 0
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 2issue.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 2issue.properties["myProperty"] ?? "default value"
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 2issue.comments.length > 0
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 2let 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;
.
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 2issue.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 2issue.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); });
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)
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 2issue.comments.map(c => { author: c.author, body: c.body.plainText })
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 2issue.comments.length > 0 ? issue.comments[0].author : null
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 2numbers.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.
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 2try { <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 2try { <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 } }
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 2issue.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 2user.accountId == project.properties['special-user'].accountId
Check if the issue type is either a Bug or Task (using a regular expression):
1 2issue.issueType.name.match('^(Bug|Task)$') != null
Retrieve IDs of all linked issues along with the link name:
1 2issue.links.map(link => { name: link.type[link.direction], issue: link.linkedIssue.id })
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 2issues.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 } }
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 2typeof project == 'undefined' || project.style == 'next-gen'
The above will return true in any of these occur:
There are two ways to interact with Jira entities:
Loading an object is done with type constructors. For example, to get a summary of an issue with key HSP-1, write:
1 2new 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.
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.
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 2JSON.stringify(issue.customfield_10000).includes('My searched string')
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:
toISOString()
method. This will return a string in the ISO 8601 extended format. For example, issue.created.toISOString()
.issue.created
.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 2issue.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:
new Date()
).new Date().minusDays(3)
is the current date minus three days.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.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:
moduleKey
: The key of the module that provided the expression.
Currently, this is available for workflow conditions and validators,
but will not be included for expressions from web conditions.condition.id
: The ID of the evaluated workflow condition.validator.id
: The ID of the evaluated workflow validator.workflow.name
: The name of the workflow the evaluated condition or validator belong to.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.
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:
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)
.
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.
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.
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 2issues.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:
List<{attachments: Number, comments: List<Comment>, key: String}>
, that is
it returns a List where each item is a Map with these properties:
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.
Rate this page: