Display conditions
Modules

Rate this page:

Jira custom field (beta)

The jira:customField module creates a new custom field in Jira, which makes it easier for users to add information to issues, specific to their teams' needs.

This module can only be used in Jira company-managed projects.

Data types

Each field type has to be based on a predefined data type.

The data type controls what kind of values the REST API accepts and returns for the field, the field's behavior in JQL, and the default rendering behavior.

The available data types are:

  • string - values represent plain strings. JQL offers autocomplete and exact comparison.
  • number - values represent numbers (double-precision 64-bit IEEE 754 floating points).
  • user - values represent users identified by Atlassian account IDs. The field behaves like any other user field in Jira when you interact with it in the UI or the REST API.
  • group - values represent groups identified by names. The field behaves like any other group field in Jira when you interact with it in the UI or the REST API.
  • object - values are arbitrary JSON objects. See below for more details.
  • datetime - values are strings that represent dates with time and timezone. The field behaves like any other datetime field in Jira when you interact with it in the UI or the REST API. However, if milliseconds are provided, they are ignored.

Object type

The object type makes use of additional properties compared to other types.

The required formatter property contains a Jira expression that returns a string. This string is used to represent the value if the rendering function is not provided, or where the rendering function is not supported. For example, the column view in the Global Issue Navigator.

The optional schema property contains the JSON schema that is used to validate values stored in the field.

By default, you can search for a text string anywhere in the content of an object-type field. To enable more fine-grained searching, create Field properties with the searchAlias schema property .

This means that, for a field named X:

  • using the default behavior you can search the entire value stored in this field using X ~ "text to search for"
  • by defining a field property, such as Y, in the schema with searchAlias, you can search that field property using X.Y = value.

The JQL type of each property created with searchAlias is derived from the schema type, using these rules:

  • string schema type becomes a string in JQL.
  • number schema type becomes a number in JQL.
  • intger schema type becomes a number in JQL.
  • boolean schema type becomes a string in JQL.
  • array schema type becomes an array of items of a type following these rules. For example, an array containing items with the schema type integer becomes an array of items with the type number in JQL.
  • search aliases for any other schema type are ignored.

It's possible to override the schema type with searchType. This enables you to use the complex types supported in JQL but not available in the JSON schema. The following search types are available:

Note: pairs of conflicting field properties, those with the same name and path but different types, are ignored.

Object type JQL search example

This is an example of a field that stores money. It showcases the use of:

  • the formatter and schema properties.
  • JQL search aliases.
1
2
  jira:customField:
    - key: cf-type-money
      name: Development cost
      description: Tracks the development cost of features, in different currencies
      type: object
      formatter: 
        expression: "`${value.amount} ${value.currency}`"
      schema:
        properties:
          amount:
            type: number
            searchAlias: Amount
          currency:
            type: string
            enum: [ "USD", "EURO", "AUD" ]
            searchAlias: Currency
          spender:
            type: string
            searchType: user
            searchAlias: Spender
        required: [ "amount", "currency" ]

The schema ensures that values of this field look like this:

1
2
{
  "amount": 100,
  "currency": "USD",
  "spender": "<account-id>"
}

With the formatter in place, displayed on the issue search, the value will be a string 100 USD.

In addition to the default text search of the entire field value, more fine-grained search using these field properties is available:

  • Amount (JQL type: number),
  • Currency (JQL type: string),
  • Spender (JQL type: user).

Collection types

A Forge custom field can store a collection of up to 100 values. This is done by declaring the collection property, which specifies the collection type. For example, to create a field that stores a list of strings, declare it as:

1
2
type: string
collection: list

These data types can be part of a collection:

  • string
  • group
  • user

Note, using any other data type in combination with collection:list prevents the field from appearing in Jira.

Field lifecycle

A locked instance of each custom field defined in the manifest is created upon deployment. For the field to be visible on issues, a Jira admin must first add it to screens.

The field disappears when the app is uninstalled or if the field is removed from the manifest. If the app is reinstalled within 30 days, or the field is restored, the field will reappear with all the previous data intact. All changes made to the field in the manifest (e.g. changing the name) are reflected in Jira after the app is deployed. (Warning: changing the data type after the field has been used with the old type is not supported, and will result in undefined behavior, potentially making the field unusable.)

The type of the field (accessible through the schema.custom property in the response of the Get fields REST API) is equal to the exact ID of the module, that is: ari:cloud:ecosystem::extension/{app-id}/{environment-id}/static/{module-key}, where:

  • {app-id} is your app ID defined in the manifest.
  • {environment-id} is the ID associated with the installation environment.
  • {module-key} is the key of the custom field module defined in the manifest.

If you need to fetch this ID in your app (for example, to identify the field in the response of the Get fields REST API), use the localId property from the product context. It has the following format:

1
2
ari:cloud:ecosystem::extension/<app-id>/<environment-id>/static/<invoked-module-key>

You can use it to create the ID of your custom field type. There are two ways to get the product context, depending on your app's rendering method:

Read-only fields

Read-only fields are those that have the read-only set to true in their definition in the manifest. They aren't editable by users, neither through the UI nor the Edit issue REST API.

They are great for showing values derived from the issue state. While you can show anything using issue panels or issue glances, custom fields may be a better choice if you need all their benefits, such as JQL search, configuration on screens, or being part of the issue import and export.

The value of such fields has to be calculated beforehand and set with the dedicated REST API. One pattern to achieve this is to listen to product triggers for events that may affect the values and update the values as necessary.

Additionally, updating the value on every issue view can be achieved with the value function.

Validation

A field value can be validated with Jira expressions.

Validation takes place whenever an issue is edited or created, but not when the app updates the value directly with the private field update REST API.

The following context variables are available in the validation expression:

  • user (User): The user that wants to modify the field value.

  • issue (Issue): The edited issue.

  • project (Project): The project the issue belongs to.

  • configuration: The configuration stored against the custom field context. Typically a map. Depending on the value of the configuration data, this may also be any primitive (number, string, boolean) or a list.

  • value: The value that's being set on the field. The type of this variable depends on the data type of the field and can be one of the following:

    • String, if the data type is string or group.
    • Number, if the data type is number.
    • User, if the data type is user.
    • Map, if the data type is object.
    • Date, if the data type is datetime.

    If the field stores a collection, the value type will be a List with items of one of the types specified above.

The expression can return three types of values:

  • true means that the validation was successful, and the operation is allowed.
  • false means that the value is invalid. The error message defined in the manifest's validation.errorMessage property will be shown to the user.
  • String means that the value is invalid. The returned string will be shown as the error message to the user.

Validation example 1

Allow only numbers between 0 and 100, plus empty (null) values:

1
2
validation:
  expression: value == null || value >= 0 && value <= 100
  errorMessage: Only values between 0 and 100 are allowed.

Validation example 2

Allow only users from group field-editors to edit the value of the field, unless it's a new issue being created, and the field value is empty.

1
2
validation:
  expression: |-
    let isIssueCreate = issue == null || issue.id == null;
    let isEmpty = value == null; 
    let hasPermission = user.groups().includes('field-editors');
    isIssueCreate && isEmpty || hasPermission
  errorMessage: You don't have permission to edit this field. 

See the documentation for Jira expressions to find out what else is possible.

Rendering

Forge apps can provide rendering of the field by specifying the function property.

The rendering function must return a CustomField UI kit component. Inside the function, you can obtain the value, ID, and type of the field from the useProductContext hook, like this:

1
2
const { extensionContext: { fieldValue, fieldId, fieldType } = useProductContext();

Editing

Forge apps can optionally provide their own editing experience of the field by specifying the edit property.

UI kit

The edit function must return a CustomFieldEdit UI kit component. Inside the function, you can obtain the current value, id, and full type of the field from the useProductContext hook, like this:

1
2
const { extensionContext: { fieldValue, fieldId, fieldType } = useProductContext();

Default editing, appropriate to the field's data type, is used if the edit function is not provided.

Check out this example app to see custom editing in action: Risk assessment custom field.

Custom UI

You can obtain the current value of the field from the getContext API, like this:

1
2
const { fieldValue } = await view.getContext();

To update the field value, use the submit API, like this:

1
2
await view.submit(fieldValue);

Default editing, appropriate to the field's data type, is used if the edit resource is not provided.

Issue creation dialog

Jira custom fields created by Forge apps that have editing functions defined can be added to the new create issue dialog by users with the appropriate permission. Validation of these fields is performed when the create issue form is submitted. Note, the rendering function must also be defined for the old create issue dialog.

Your app can find out which view a field is rendered on using the renderContext property from the product context:

1
2
const { extensionContext: { renderContext } = useProductContext();

Formatter

The formatter is used to render the field on views where rendering with functions is not supported, for example in email notifications or on the global issue navigator page.

Formatters are declared with Jira expressions, making them fast enough to be invoked synchronously whenever a rendered human-readable field value is required.

The following context variables are available in the formatter expression:

  • user (User): The current user.

  • issue (Issue): The edited issue.

  • project (Project): The project to which the issue belongs.

  • configuration: The configuration stored against the custom field context. Typically a map. Depending on the value of the configuration data, this may also be any primitive (number, string, boolean) or a list.

  • value: The field's value. The type of this variable depends on the data type of the field and can be one of the following:

    • String, if the data type is string or group.
    • Number, if the data type is number.
    • User, if the data type is user.
    • Map, if the data type is object.
    • Date, if the data type is datetime.

    If the field stores a collection, the value type will be a List with items of one of the types specified above.

Formatter example 1

In the simplest possible case, your formatter will just transform the current value without requiring any additional information. For example, you can render a text-based progress bar for a field that stores progress as a number between 0 and 100:

1
2
formatter:
  expression: "`${'▰'.repeat(value / 10).padEnd(10, '▱')} (${value}%)`"

Formatter example 2

Imagine you have a string field whose values are IDs of some external components. A Forge function that renders this field makes a call to your service to resolve those IDs into human-readable names. While such external calls are not possible in Jira expressions, you can store the mapping between IDs and names in an entity property, and read it in the formatter:

1
2
formatter:
  expression: |- 
    let mapping = project.properties.get('idToNameMapping');
    mapping.get(value)

Since your app stores the data required to format the field on the Jira side, the user experience remains fast and reliable.

See the documentation for Jira expressions to find out what else is possible.

Value Function

Forge apps can provide a value function that computes the value of the field.

This function is invoked on every issue view, so that the freshly computed value is shown to the user whenever they open the issue page. Jira also saves this value into the database against the issue. Thanks to this, the value is not only available for that one particular user interaction, but also becomes immediately refreshed for the REST API, all other views, JQL search, etc.

This is useful mostly for read-only fields that show some derived data. Because the value function is invoked only on the issue view, you will most likely still need to listen to events and update values asynchronously. However, you can at least guarantee that users will always see a fresh value on the issue view.

Manifest example:

1
2
  jira:customField:
    - key: field-key
      name: Name
      type: string
      value:
        function: computeValue

The function receives a list of issue IDs and returns a list of values for those issues. Context configuration is provided for the issues, so the app does not have to load context configuration in the function.

Here is a payload example:

1
2
{
  "field": {
    "id": "customfield_10000",
    "key": "4886f49a-482d-41ba-bd73-6aeb5ee40e0f__DEVELOPMENT__my-custom-field-key",
    "type": "ari:cloud:ecosystem::extension/4886f49a-482d-41ba-bd73-6aec5ee40e0f/4e9fe62d-2575-4b2d-b0d7-5a37f43a80a9/static/my-custom-field-key"
  },
  "issues":[
    {
      "id":10001,
      "context":10136,
      "value": "value1"
    },
    {
      "id":10002,
      "context":10136,
      "value": "value2"
    }
  ],
  "contexts": [
    {
      "id": "10136",
      "configuration": "custom data"
    }
  ]
}

Here is an example of how to define the value function:

1
2
    function valueFunctionName(arg) {
        return arg.issues.map(issue => computeCurrentValue(arg.field, issue.id));
    }

    function computeCurrentValue(field, issueId) {
       // return the value of the field for the given issue ID
    }

The values returned must be compatible with the format expected by the Edit Issue REST API operation.

For example, for the user type field the value must be an object that contains accountId. So the function would have to return a list of values like this:

1
2
[{
  "accountId": "<user-1>"
}, {
  "accountId": "<user-2>"
}, {
  "accountId": "<user-1>"
}]

The order of values returned must be the same as the order of issues received in the argument.

Note that the issue view is the only view where the value function is invoked. However, the function is still expected to work with multiple issues at once (accepting and returning lists). Other views may become supported, including ones that display more than one issue at a time. Therefore, make sure your function has constant performance regardless of the number of issues in the argument.

Rendering hierarchy

The rendering hierarchy goes from functions (highest priority), to formatter expressions, to default rendering (lowest priority).

On views that support rendering with functions, the function is used if defined. Otherwise the formatter expression is used. If neither is defined, the default rendering for the field's data type is used.

On the issue view, if the value function is defined, it is invoked first, then its result is passed on to the rendering function, formatter expression, or the default rendering component.

On views that don't support rendering with functions, the formatter expression is used if defined, otherwise the raw field value is shown.

Rendering example

To see custom rendering in action, check out the Issue Progress Custom Field example app.

Search suggestions

When writing JQL queries in advanced search, users expect to receive automatic suggestions for valid values. By default, we try to provide these suggestions automatically for Forge custom fields, differently for each data type. For example, for fields of type user, we compile the list of suggestions from the pool of all Jira users.

Apps can override this default behavior and provide their own custom suggestions. This is achieved by adding the searchSuggestions section into the field definition in the manifest. The suggestion provider can be either a Jira expression, or a Forge function.

In the search suggestions function, calls to an Atlassian API must be done as the app developer by using api.asApp(). Making requests on behalf of a user with api.asUser() won’t work.

Input

The Jira expression has access to the following context variables:

  • user (User): The user who is receiving the suggestions.
  • query (String): The string that the user already typed into the editor as the value of the field. Use it to narrow down the list of suggestions.

Similarly, the function receives an argument object with the same set of information:

1
2
{
  "query": "<the value entered by the user>",
  "user": {
    "accountId": "<the ID of the user that is requesting the suggestions>"
  },
  "context": {
    "cloudId": "<the ID of the instance the app is running on>"
  }
}

The following example shows a signature that can serve as a starting point for your function's implementation:

1
2
export function generateSearchSuggestions({ query, user: { accountId }, context: { cloudId }}) {
    // your implementation goes here
}

Output

The function or expression must return a list of suggestions. Each suggestion can be either a plain string, or an object that contains the value that’s used in JQL when the user selects the suggestion, and a label that the user sees on the list:

1
2
[
  {
    "value": "340f8126-50f7-4fe7-8765-1a151678b917",
    "label": "Value 1"
  },
  {
    "value": "771617fc-7417-4a4a-8b47-9230bbac47a0",
    "label": "Value2 2"
  }
]

If your values are already human-readable, you can just return plain strings:

1
2
["Value 1", "Value 2", "Value 3"]

Properties

PropertyTypeRequiredDescription
key

string

Yes A key for the module, which other modules can refer to. Must be unique within the manifest.

This key becomes a part of the custom field key as described in the Field lifecycle section.

Regex: ^[a-zA-Z0-9_-]+$

namestringYesThe name of the field.
descriptionstringYesThe description of the field.
typestringYesThe type of the value stored by the field. Available types are:
  • string
  • number
  • user
  • group
  • datetime
collectionnone|list (default: none) The kind of collection that the field values should be stored in. See collection types for more details.
validation:
  expression
stringA Jira expression that validates the field value. See validation for more details.
validation:
  errorMessage
stringThe error message to show when the validation expression returns false.
formatter:
  expression
stringRequired for the object type; otherwise, optionalA Jira expression that renders the value as a string. See formatter for more details.
value:
  function
string A function that computes the value of the field.

See value function for more details.

schemaobjectAllowed only for the object typeA JSON schema that desribes values stored in the field.
searchSuggestions:
  expression
stringRequires either function or expression. Only one of the two properties must be present. A Jira expression that provides value suggestions in advanced search. See search suggestions for more details.
searchSuggestions:
  function
stringRequires either function or expression. Only one of the two properties must be present. A reference to the function module that provides value suggestions in advanced search. See search suggestions for more details.
readOnlybooleanWhether or not the field is read-only. Read-only fields can't be edited by users. Defaults to false.
functionstringA reference to the function module that provides the field rendering. This function must return the CustomField component.
resolver{ function: string } Contains a function property, which references the function module that defines the configuration of the resource property. Can only be set if the module uses the resource property.
edit:
 function
stringA reference to the function module that provides the field editing experience.
edit:
 resource
string A reference to the static resources entry that your edit entry point wants to display. See Resources for more details. To submit the view, use the submit API.
displayConditionsobjectThe object that defines whether or not the field is displayed on the issue view (other views or REST APIs are not affected). See display conditions.

Extension context

Custom UI

PropertyTypeDescription
typestringThe type of the module.
issue:
  id
stringThe id of the issue on which the module is rendered.
issue:
  key
stringThe key of the issue on which the module is rendered.
issue:
  type
stringThe type of the issue on which the module is rendered.
issue:
  typeId
stringThe id of the type of the issue on which the module is rendered.
project:
  id
stringThe id of the project where the module is rendered.
project:
  key
stringThe key of the project where the module is rendered.
project:
  type
stringThe type of the project where the module is rendered.
fieldValuestring|string[]|number|objectThe value of the field.
fieldIdstringThe id of the field.
fieldTypestringThe type of the field.
renderContextissue-view|issue-createThe context on which the extension is rendered.
entryPointedit|undefinedThe entry point of the module.

UI kit

PropertyTypeDescription
typestringThe type of the module.
fieldValuestring|string[]|number|objectThe value of the field.
fieldIdstringThe id of the field.
fieldTypestringThe type of the field.
renderContextissue-view|issue-createThe context on which the extension is rendered.
issueIdstringThe id of the issue on which the module is rendered.
issueKeystringThe key of the issue on which the module is rendered.
issueTypestringThe type of the issue on which the module is rendered.
issueTypeIdstringThe id of the type of the issue on which the module is rendered.
projectIdstringThe id of the project where the module is rendered.
projectKeystringThe key of the project where the module is rendered.
projectTypestringThe type of the project where which the module is rendered.
entryPointedit|undefinedThe entry point of the module.

Example

The following example declares a number-type field that represents issue progress:

1
2
jira:customField:
  - key: issue-progress
    name: Issue progress
    description: The progress of the issue, on a scale from 0 to 100.
    type: number
    validation:
      expression: value == null || (value >= 0 && value <= 100)
      errorMessage: The value must be a number between 0 and 100.
    formatter:
      expression: "`${'▰'.repeat(value / 10).padEnd(10, '▱')} (${value}%)`"
    searchSuggestions:
      expression: '["0", "25", "50", "75", 100"]'
    readOnly: false
    function: renderIssueProgress
    edit:
      function: editIssueProgress

Rate this page: