# Building a Description Templates App for Jira with Atlassian Forge

If your team creates a lot of Jira issues, you've probably noticed that the description field is almost always blank. People fill it in differently every time or not at all. This post covers how I built a Forge app that pre-fills the description field in Jira's create dialog based on the issue type, so teams always start from a consistent template.

## What it does

*   Project admins configure rich text templates per issue type in Project Settings
    
*   When anyone opens the "Create issue" dialog for a configured issue type, the description is automatically pre-filled with the template
    
*   Users can freely edit it before submitting, it's just a starting point
    

The app appears in the project sidebar under **Apps → Description Templates**.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/1a0dfd2a-28d7-492d-813b-8f4a787521da.png align="center")

* * *

## Tech stack

*   **Atlassian Forge** :: serverless platform for building Jira/Confluence apps
    
*   **Forge UI Kit 2** :: React-based component library (`@forge/react`)
    
*   **Jira UI Modifications API** :: the mechanism that injects content into the create dialog
    
*   **Forge Storage** :: key-value store for persisting templates
    

* * *

## The UI

### Empty state

When no templates are configured, the page shows a clear empty state with an "Add template" button in the top right , one clear call to action, no clutter.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/000ac4a8-92a8-4a4c-a58f-ec410a2ab841.png align="center")

### Adding a template

Clicking "Add template" opens the add view. You pick a work type from a dropdown (only unconfigured types appear),

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/bc7b5d7b-52f8-4f38-a891-a64b282b1574.png align="center")

then write the template in a full rich text editor, the same `CommentEditor` component Jira uses natively. You get headings, lists, code blocks, links, colors, and more.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/58cbf961-a4c9-419e-9d37-987a27fe81c0.png align="center")

### List view with Edit and Delete

Once saved, the template appears in the list with **Edit** and **Delete** actions. A success banner confirms the save. Each configured work type gets its own row.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/19ace3c6-217b-4a00-a771-fae23b71d367.png align="center")

The "Add template" button stays visible for any remaining unconfigured types.

### Editing an existing template

Clicking Edit takes you straight to the editor pre-filled with the existing template. No need to re-select the work type.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/51f31a7d-0fff-4dfd-a59c-b4f69c6f9b7a.png align="center")

You can also toggle to a **Preview** mode to see how the template will render before saving.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/47c64c31-ca9f-4fcc-88d5-6ee6ff6e2fe6.png align="center")

* * *

## The payoff - create dialog pre-fill

When a user opens the create dialog for a configured issue type, the description field is already filled in with the template. They just fill in the blanks. Zero extra clicks.

![](https://cdn.hashnode.com/uploads/covers/69b71990f4eb2f8b04ec0ee4/a56b3ad6-4092-4f6a-bf7b-134f42cb42d6.png align="center")

* * *

## Architecture

The app has three parts:

### 1\. Settings page (`jira:projectSettingsPage`)

A React UI (UI Kit 2) where admins pick a work type and write a template using `CommentEditor`. Templates are saved to Forge Storage and a UI Modification is registered via the Jira REST API.

### 2\. Resolver functions

Serverless functions that handle:

*   `getIssueTypesWithTemplates` : fetches issue types for the project and merges in saved templates
    
*   `saveTemplate` : persists the ADF to storage and registers/updates/deletes the UI Modification
    

### 3\. UIM script (`jira:uiModifications`)

A lightweight browser bundle that runs when the create dialog opens. It reads the ADF from the registered UI Modification and calls `api.getFieldById('description').setValue(adf)`.

* * *

## Key lessons learned

### 1\. Always call `ForgeReconciler.render()`

UI Kit 2 apps show a skeleton forever if you forget this at the bottom of your entry file:

```js
ForgeReconciler.render(<App />);
```

It's easy to miss when starting from scratch.

### 2\. Use `asUser()` for project reads, `asApp()` for UI Modification CRUD

The Jira UI Modifications API requires app-level credentials , `asUser()` returns 403. But reading project data works better with `asUser()` since it uses the logged-in user's permissions.

```js
// Fetch issue types - use asUser()
const res = await asUser().requestJira(route`/rest/api/3/project/${projectId}`, {
  headers: { Accept: 'application/json' },
});

// Register UI Modification - use asApp()
const postRes = await asApp().requestJira(route`/rest/api/3/uiModifications`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify(payload),
});
```

### 3\. Always use the `route` tagged template literal

```js
// ❌ Wrong - throws "You must create your route using the 'route' export"
await requestJira(`/rest/api/3/project/${id}`);

// ✅ Correct
import { route } from '@forge/api';
await requestJira(route`/rest/api/3/project/${id}`);
```

### 4\. `viewType` must be `GIC`, not `CREATE_ISSUE`

The correct viewType for the create dialog is `GIC` (Global Issue Create). Using `CREATE_ISSUE` returns a 400 Bad Request with a confusing error message.

```js
contexts: [{ projectId, issueTypeId, viewType: 'GIC' }]
```

And in `manifest.yml`:

```yaml
jira:uiModifications:
  - key: description-template-uim
    resource: uim-resource
    viewType:
      - GIC
```

Without `viewType` in the manifest, Jira refuses to load the UIM script and shows: *"We couldn't load some of the UI modifications apps for this page, because they don't have required scopes."* , a misleading error that actually means the module isn't configured correctly.

### 5\. Don't mix classic and granular scopes

Mixing them causes UIM scripts to silently fail to load. Stick to classic scopes only:

```yaml
permissions:
  scopes:
    - read:jira-user
    - read:jira-work
    - write:jira-work
    - manage:jira-configuration
    - storage:app
```

### 6\. The UIM `onInit` callback must be synchronous

`uiModificationsApi.onInit` doesn't await promises. If you pass an async function, `invoke` calls will never resolve and the field won't be set. Keep it synchronous and read the ADF directly from `uiModifications[0].data`:

```js
import { uiModificationsApi } from '@forge/jira-bridge';

uiModificationsApi.onInit(
  ({ api, uiModifications }) => {
    if (!uiModifications?.length) return;
    const rawData = uiModifications[0].data;
    if (!rawData) return;
    let adf;
    try { adf = JSON.parse(rawData); } catch { return; }
    api.getFieldById('description')?.setValue(adf);
  },
  () => ['description']
);
```

### 7\. For team-managed projects, use the project endpoint for issue types

The global `/rest/api/3/issuetype` endpoint returns an empty array for team-managed (next-gen) projects. Fetch issue types from the project endpoint instead:

```js
const res = await asUser().requestJira(route`/rest/api/3/project/${projectId}`);
const body = await res.json();
const issueTypes = body.issueTypes.filter((it) => !it.subtask);
```

* * *

## Wrapping up

The combination of Forge Storage + Jira UI Modifications is a powerful pattern for contextual defaults in Jira. The main gotchas are around scopes, the `viewType` value, and keeping the UIM script synchronous. Once those are sorted, the result is seamless and users get a pre-filled description the moment they open the create dialog, with no extra clicks required.
