RFC 125: UI Modification for Forge Custom Fields

RFC: UI Modification for Forge Custom Fields

RFCs are a way for Atlassian to share what we’re working on with our valued developer community. It’s a document for building shared understanding of a topic. It expresses a technical solution, but can also communicate how it should be built or even document standards. The most important aspect of an RFC is that a written specification facilitates feedback and drives consensus. It is not a tool for approving or committing to ideas, but more so a collaborative practice to shape an idea and to find serious flaws early.

Please respect our community guidelines: keep it welcoming and safe by commenting on the idea not the people (especially the author); keep it tidy by keeping on topic; empower the community by keeping comments constructive. Thanks!


Project Summary

We are proposing to enable UI Modifications (UIM) for Forge Custom Fields (FCF) (jira:customField and jira:customFieldType modules) in Jira Cloud. This will allow apps to programmatically modify the appearance and behavior of Forge Custom Fields in Issue View, Global Issue Create, and Issue Transition screens. Our goal is to unlock new extensibility scenarios for partners, enabling richer, more dynamic field experiences for end users.

  • Publish: 16 January, 2026

  • Discuss: 30 January, 2026

  • Resolve: 20 February, 2026

Problem

Forge Custom Fields are a powerful way for apps to introduce new field types and custom data into Jira issues. However, until now, partners have had limited ability to programmatically modify the UI of these fields after creation. This restricts use cases such as:

  • Dynamically hiding/showing fields based on context

  • Changing field names/descriptions on the fly

  • Setting field values programmatically in response to other changes

  • Making fields read-only or required based on business logic

Without UI Modifications, partners must rely on static field definitions, limiting the flexibility and interactivity of their solutions.

Proposed Solution

We propose to support UI Modifications for Forge Custom Fields, allowing apps to:

  • Set or get the field’s name, description, visibility, value, and read-only/required state at runtime

  • Listen for changes to field values and react accordingly

  • Apply these modifications in Issue View, Global Issue Create, and Issue Transition screens

Planned API Changes

To enable UI Modifications for Forge Custom Fields, we plan to introduce new APIs and update existing ones. The following changes are proposed:

1. New/Updated APIs for Field Data Access

  • A new API (e.g., customFieldApi.getFieldData()) will be introduced for Forge Custom Fields to read and react to field values that are set by UI Modifications.

  • This will replace the current pattern of using view.getContext() for field value access, as the latter is not reactive to UI Modifications. Please note that the view.getContext() method remains available with no changes. We are planning to add a new API for apps onboarding their Forge Custom Fields to UI Modifications.

2. Manifest Changes

  • App developers will need to add a new flag (e.g., isUIModificationsEnabled: true) to their Forge Custom Field or Custom Field Type module in manifest.yml to opt-in to UI Modifications support.

3. UI Modifications API

Example: Proposed App Code Changes

Before (current pattern):

import React, { useEffect, useState } from 'react';
import { view } from '@forge/bridge';

export function MyCustomFieldComponent() {
  const [fieldValue, setFieldValue] = useState();
  useEffect(() => {
    view.getContext().then((context) => {
      setFieldValue(context.extension.fieldValue);
    });
  }, []);
  return (
    <div>
      <p>Current field value: {String(fieldValue)}</p>
    </div>
  );
}

After (proposed pattern):

import React, { useEffect, useState } from 'react';
import { customFieldApi } from '@forge/jira-bridge';

export function MyCustomFieldComponent() {
  const [fieldValue, setFieldValue] = useState();
  useEffect(() => {
    // Subscribe to field data updates (initial value + subsequent changes)
    customFieldApi.getFieldData(({ fieldValue }) => {
      setFieldValue(fieldValue);
    });
  }, []);
  return (
    <div>
      <p>Current field value: {String(fieldValue)}</p>
    </div>
  );
}

Note - The view.getContext() method remains available with no changes. We are planning to add a new method for apps onboarding their Forge Custom Fields to UI Modifications.

Manifest change:

modules:
  jira:customField:
    - key: my-custom-field
      name: My Field Name
      type: string
      isUIModificationsEnabled: true
      # ...other config

User Experience

  • End users will see Forge Custom Fields update dynamically in response to app logic, without page reloads.

  • Apps can tailor field behavior and appearance of Forge Custom Fields to the context of the issue, project, or user.

Developer Experience

  • Simple API for reading and writing field data, and for listening to changes.

  • No need for workarounds or static field definitions—field UI can be fully dynamic.

  • Clear migration path: update your manifest and field view code to adopt the new APIs.

Open Questions & Asks

While we would appreciate any reactions you have to this RFC, we’re especially interested in learning more about:

  • Which Forge Custom Field types would you most like to see supported for UI Modifications?

  • Are there additional UI modification APIs or capabilities you would like to see?

  • Do you have concerns about the proposed API or developer experience?

  • Are there scenarios where you would want to restrict or audit UI modifications?

  • Would you be interested in 1:1 conversations to discuss your use cases in more detail?

How to Give Feedback

  • Comment directly on this RFC thread.

  • Share your use cases, concerns, and suggestions to help shape the future of Forge Custom Field extensibility.


Disclaimer: RFCs are a way for Atlassian to share what we’re working on with our valued developer community. It’s a document for building shared understanding of a topic. It expresses a technical solution, but can also communicate how it should be built or even document standards. The most important aspect of an RFC is that a written specification facilitates feedback and drives consensus. It is not a tool for approving or committing to ideas, but more so a collaborative practice to shape an idea and to find serious flaws early.


10 Likes

Why change the modern promise API to a more old fashioned callback approach? The promise API would work equally nice and allows error handling.

  const { fieldValue } = await customFieldApi.getFieldData();
  setFieldValue(fieldValue);

AFAIK this works in any hook of contemporary react versions.

Where does setFieldValue come from?

3 Likes

Hi there,

thanks for sharing!

Which Forge Custom Field types?
Object (JSON) types. This is our highest priority for managing complex data with simple UIs.

Concerns / DX

  • Performance: Forge fields can already be slow. I am concerned about “pop-in” / layout shifts if setVisible/ setValue takes time to resolve.

  • Tunneling: Full support in forge tunnel is essential for debugging reactive logic.

Questions

  1. Scope: Can a Forge field listen to/update other fields (native Jira fields or other Forge fields), or is it isolated to itself?

  2. Visibility: Can we programmatically setValue on a field even if we have hidden it via setVisible(false)? Can a field become visible if it is not on the appropriate screen?

  3. Safety: Is there built-in cycle detection to prevent infinite loops (Field A updates B → B updates A)?

  4. Auditing: Will changes made via UI Modifications be distinguishable in the Issue History from standard user edits?

We’re still heavily missing native rendering in List (Jira) / Search ( Jira ) / Boards ( Jira ) / Bulk Edit ( Jira ). UI modifications will further widen this gap.

I would love to discuss this in 1:1 conversation :slightly_smiling_face:

Cheers,
paul

3 Likes

Thanks for sharing this! I hope this functionality helps retrieve JSM-related fields, such as the work item’s Request Type, on the fly. I am very much interested in having a 1:1 conversation to discuss further. As a reminder, my use-case is below:

I have a custom field called “Request Category,” a single-select dropdown that dynamically displays request subtypes for each request type. For example:

Request Type: Dropbox:
Request Category options:

  • New Account
  • Troubleshooting

If I were to change the request type to “Jira,” the options for Request Category would be the following:

  • Access to Space
  • Remove a user from Space

Currently, I implement this through the edit.jsx file of my Request Category custom field:

import React, { useState, useCallback, useEffect } from 'react';
import ForgeReconciler, { Select } from '@forge/react';
import { CustomFieldEdit } from '@forge/react/jira';
import { invoke, view } from '@forge/bridge';

const Edit = () => {
    const [value, setValue] = useState({});
    const [categories, setCategories] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    const onSubmit = useCallback(async () => {
        try {
            const valueToSubmit = (value?.label && value?.value) ? value : null;

            await view.submit(valueToSubmit);
        } catch (e) {
            console.error(e);
        }
    }, [view, value]);

    useEffect(() => {
        const loadContext = async () => {
            try {
                const context = await view.getContext();
                const ext = context.extension ?? {};
                const {
                    issue, project,
                    fieldValue
                } = ext;

                let requestTypeId;
                try {
                    const key = issue.key;
                    requestTypeId = await invoke('getRequestTypeId', { issueKey: key, forceRefresh: true });
                } catch (error) {
                    console.log(`Error fetching requestTypeId: ${error}`)
                }

                console.log(`Fetching categories for project ID ${project.key} and request type ID ${requestTypeId}...`);
                let categories_fetch = await invoke('getCategories', { projectId: project.key, requestTypeId });
                console.log(`categories: ${JSON.stringify(categories_fetch)}`);

                setCategories(categories_fetch);
                setIsLoading(false); // Set loading to false after data is loaded

                // Set the initial value if it exists
                if (fieldValue) {
                    console.log(`Setting initial field value: ${JSON.stringify(fieldValue)}`);
                    try {
                        // Try to parse as JSON first
                        const parsedValue = JSON.parse(fieldValue);
                        setValue(parsedValue);
                    } catch (error) {
                        // If not JSON, find matching category by ID
                        const matchingCategory = categories_fetch.find(cat => cat.id === fieldValue);
                        if (matchingCategory) {
                            setValue(matchingCategory);
                        } else {
                            setValue({ id: fieldValue, name: `Unknown (${fieldValue})` });
                        }
                    }
                }
            } catch (error) {
                console.error('Error loading context:', error);
                setIsLoading(false); // Set loading to false even on error
            }
        };

        loadContext();
    }, []);

    const selectOptions = categories.map(cat => ({ label: cat.name, value: cat.id }));

    const handleOnChange = useCallback((selectedOption) => {
        setValue(selectedOption);
    }, []);

    if (isLoading) {
        return (
            <CustomFieldEdit onSubmit={onSubmit} hideActionButtons>
                <Select
                    appearance="default"
                    options={[]}
                    isDisabled={true}
                    placeholder="Loading categories..."
                />
            </CustomFieldEdit>
        );
    }

    return (
        <CustomFieldEdit onSubmit={onSubmit} hideActionButtons>
            <Select
                appearance="default"
                options={selectOptions}
                onChange={handleOnChange}
                value={selectOptions.find(option => option.value === value?.id)}
                placeholder="Choose a category..."
            />
        </CustomFieldEdit>
    );
};

ForgeReconciler.render(
    <React.StrictMode>
        <Edit />
    </React.StrictMode>
);

The problem is, I cannot retrieve the request type ID from the current view.getContext(); The issue object only returns the following info:
{“key”:“SPACE-364”,“id”:“32435”,“type”:“Service Request”,“typeId”:“10502”}

The only way I can retrieve the request type ID on demand is by calling the /rest/servicedeskapi/request endpoint, which is too computationally expensive for my use case (it causes my field to load for ~3 seconds longer than it should).

I have looked into storing the request type ID in KVS using a backgroundScript module and updating the key/value pair when the request type changes; however, the event listeners for issue changes do not fire immediately, causing a significant delay in updating the stored value.

What I hope to get out of this ticket is a way to retrieve JSM-related context fields, such as Request Type, via view.getContext() or another method that does not require querying an endpoint.

Perhaps with this change, I will be able to modify the options provided in edit.jsx using the uiModificationsApi (without changing the options globally, as each request may have a different request type, and thus, require a different set of dropdown options).

return (
    <CustomFieldEdit onSubmit={onSubmit} hideActionButtons>
        <Select
            appearance="default"
            options={selectOptions}
            onChange={handleOnChange}
            value={selectOptions.find(option => option.value === value?.id)}
            placeholder="Choose a category..."
        />
    </CustomFieldEdit>
);

JSM Portal requirements very often require show/hide require/not require field values based on other field values that have been selected. We see this use case much, much more than in the JSM technician screen. Many customers are coming from previous applications that support this functionality and can’t believe it’s missing from JSM portal. The features described above would fit perfectly this use-case in the JSM portal create screen. Please release ASAP! :slight_smile: Thanks!

Thanks a lot for the thoughtful question – this is exactly the kind of feedback we were hoping for on the RFC.

Why the callback-style example?

You’re absolutely right that a promise-based version works well in React hooks and gives clearer error‑handling options.

In our example, we used a callback-style form to emphasise the reactive behaviour: customFieldApi.getFieldData(handler) is intended to:

  • Provide the initial field data, and

  • Re‑invoke the handler whenever the field value changes via UI Modifications.

So the intent is closer to “subscribe to updates” rather than a single one‑off “get”. We’ll think about how to make this reactive / subscription behaviour clearer in the documentation.

It’s also perfectly reasonable for app developers to build their own React custom hooks on top of this – for example, useFieldData or useFieldValue hooks that internally call customFieldApi.getFieldData and expose whatever interface (including promise-style helpers) best fits their app.

In the RFC snippet, setFieldValue is the standard React state setter from useState. The examples have now been updated to show the full context. This is meant to illustrate how you’d wire the subscription into React state, and how setFieldValue is defined.

I’m a bit confused, probably more than a bit.
I’ve been using React for a few years, since the days of class components and mixins, and I really think this is not the way useEffect is meant to behave.

React runs the useEffect’s setup function once (when the component commits) and then everytime the dependencies change. In your example the dependency array is empty, i.e. the setup function runs once for the lifetime of the component. I would be very surprised if I saw the field value getting updated after the initial render with the code you provided.

If what is happening behind the scenes is that customFieldApi.getFieldData creates a subscription with a socket or a polling mechanism, that’s absolutely unclear from your example and would look very surprising. The established pattern for that would be something like

useEffect(() => {
  const subscription = customFieldApi.subscribe(({fieldValue) => setFieldValue(fieldValue))
  return () => subscription.unsubscribe();
}, [])

Or, you could even use useSyncExternalStore, or a combination of useEffect and useEffectEvent.

I really don’t like the idea of opaquely creating subscriptions with no control over them: what happens if I have an editable field and a UI modification changes the value while the user is in the middle of a long and complex edit?

Moreover, in your example you’re using a plain useState, and it’s well known that the setter is stable and doesn’t need to be a useEffect dependency, but if we were using anything else (e.g. zustand) your example might fail if the setter changes after the initial render.

If we’re using React, let’s use React the way everybody uses it: even the basic example for useEffect in the docs follows that pattern useEffect – React

Of course the same concerns apply to people who are using Custom UI and don’t use React: don’t do magic in your code, stick to well-established patterns for subscriptions.

PS: I’m not a native English speaker, and it probably shows in the way I write so I might also find it tempting to use an LLM to polish my phrasing, but I find the “claudeisms” a bit off-putting. I want to engage in conversations with Atlassian and Atlassians, not with Claude’s interpretation of Atlassians’ thoughts.

1 Like

Hi @PaoloCampanelli

You’re right here. As I mentioned in my earlier comment, getFieldData is intended to behave like a subscription, not a one‑off getter, and our example doesn’t make that obvious. We’ll work on making both the API and the documentation clearer about the subscription behaviour (and how to clean it up), and align them more closely with the established patterns.

Great question! From the platform perspective, we are taking care some of the performance bottlenecks and we’ll benchmark and monitor the performance aspects.

Could you clarify what you mean by “full support in forge tunnel”?

Yes. Forge Custom Fields can listen to/update other fields.

Yes.

Could you share more details on this use case?

The UI Modifications on any fields will not re-trigger UI Modifications again. So, the infinite loops are handled.

Great question! We don’t have this feature at this moment.

Some of these features are already being worked on, and we’ll share more details when they are closer to release.

Hi @EricKruegerStrataCom! Thanks for sharing your use case with us. We’ve recently launched UI Modifications for JSM Portal - https://developer.atlassian.com/platform/forge/manifest-reference/modules/jira-service-management-ui-modifications/. I’m sure it can help solve some of the use cases you had in mind.

Please let me know if this helps. Thanks :slight_smile: