Skip to main content

Command Palette

Search for a command to run...

Building a Description Templates App for Jira with Atlassian Forge

Updated
6 min read

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.


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.

Adding a template

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

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.

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.

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.

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


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.


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:

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.

// 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

// ❌ 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.

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

And in manifest.yml:

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:

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:

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:

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.