UI Modifications (jira:uiModifications): onInit/onChange callbacks appear to not execute reliably on Global Issue Create — no console output, no invoke() reaching resolver, despite confirmed UI modification entity creation

Environment:

  • Forge app, Custom UI, jira:uiModifications module
  • Site: arzenicstech.atlassian.net (Jira Cloud)
  • Development environment (Forge dev mode)
  • Node.js runtime: nodejs24.x

What I’m trying to do:

I’m building a jira:uiModifications app to dynamically filter a “child” field’s options (customfield_11207, a native Select List single-choice field) based on a “parent” field’s value (customfield_11206, also native Select List) — live, on the Global Issue Create screen.

Setup:

  1. Manifest declares the module correctly:

yaml

jira:uiModifications:
  - key: dd-live-uim
    title: Dynamic Dependent Live Options
    resource: dd-uim-ui
    resolver:
      function: uimResolver
  1. A UI modification entity was successfully created via POST /rest/api/3/uiModifications (using api.asApp() — note: api.asUser() fails with 403 “Only apps can access this resource (impersonated requests are not allowed)”, which isn’t clearly called out in the docs for this specific endpoint):

json

{
  "name": "Parent-Child Category Live Test",
  "contexts": [{ "projectId": "10232", "issueTypeId": null, "viewType": "GIC" }],
  "data": "{\"parentFieldId\":\"customfield_11206\",\"childFieldId\":\"customfield_11207\",\"mapping\":{...}}"
}

Response: 201, entity ID returned successfully.

  1. Client code registers both fields and calls setOptionsVisibility on change:

jsx

uiModificationsApi.onChange(async ({ api, change, uiModifications }) => {
  const { current } = change;
  const changedFieldId = current.getId();
  for (const um of (uiModifications || [])) {
    const cfg = JSON.parse(um.data);
    if (changedFieldId !== cfg.parentFieldId) continue;
    const parentOptionId = current.getValue()?.id ?? null;
    const allowedChildOptionIds = cfg.mapping?.[parentOptionId] || [];
    const childField = api.getFieldById(cfg.childFieldId);
    if (childField) childField.setOptionsVisibility(allowedChildOptionIds, true);
  }
}, ({ uiModifications }) => {
  // returns both parentFieldId and childFieldId for registration
});

What I observed:

  • On the first real test, the mechanism partially worked: selecting the parent’s second value correctly filtered the child’s options. Selecting the parent’s first value did not filter correctly (both options remained visible).
  • After adding debug logging (both console.log in the browser console and invoke() calls to a backend resolver, verified deployed via forge logs and browser DevTools source search), neither produces any output — not even from onInit, which per the docs should fire unconditionally on mount.
  • I confirmed the debug code is present in the deployed bundle (verified via grep on the built JS and via DevTools “Search across files”).
  • I have been unable to locate the correct iframe context in DevTools to inspect console.log output directly (the UIM app’s iframe does not appear distinguishable among the many iframes Jira’s Global Issue Create screen loads).

Question:

  1. Is there a reliable, documented way to access the browser console output specifically for a jira:uiModifications Custom UI resource on the Global Issue Create screen (given the large number of unrelated iframes GIC loads)?
  2. Is there a known issue with onChange/onInit callbacks silently failing to execute (or being torn down before async work like invoke() completes) on GIC specifically, as opposed to IssueView?
  3. Are there known constraints around calling setOptionsVisibility inconsistently on first-selection vs subsequent selections?
  4. Is onError reliably able to surface silent conflicts (e.g., MULTIPLE_APPS_CONFLICT, APPLY_CHANGES_FOR_UNREGISTERED_FIELD) in this scenario, and is there a recommended debugging workflow for UIM apps beyond forge logs and browser console?

Any pointers to a working, minimal reproduction of onChange + setOptionsVisibility on GIC would be appreciated — happy to share the full manifest/code if useful.

Hi @VikasThakur,

Console first, you don’t need to find the frame. Messages from every iframe already show in the main console by default, the context dropdown only changes where typed expressions run. So check Selected context only is off, the level filter is on Default, the filter box is empty, and that it’s console.log and not console.debug, which sits under Verbose. If all that’s clean and onInit still prints nothing, the callback isn’t running.

First option, probably the mapping entry. An empty list with true hides every option, so both staying visible means either nothing was applied for that value or its entry lists both child ids. Log parentOptionId next to cfg.mapping[parentOptionId].

onError covers validation, MULTIPLE_APPS_CONFLICT and APPLY_CHANGES_FOR_UNREGISTERED_FIELD while changes apply. Silence there doesn’t clear code that never ran.

The asUser() 403 is by design, only Forge apps can create UI modifications.

Which Create entry point are you testing from?

Adding to Mihai’s console tips: here are a few Global Issue Create specifics that can make it look like nothing runs. They come from building on UI modifications in the Create dialog.

1. The context has to match what the dialog shows

With projectId: "10232", issueTypeId: null, viewType: "GIC", the app only loads while 10232 is the selected project in Create (a null issue type means any type). Create preselects a project, usually the one you’re in or the one you used last. If that’s a different project, nothing runs, not even onInit, until you switch to 10232. onInit runs again on every project or issue type change.

If 10232 is a service space, Create there is the agent view (GICAgentView), so a GIC context won’t match.

2. Entry point

On GIC, UIM runs from:

  • the Create button in the top nav
  • the c keyboard shortcut
  • CreateIssueModal opened from an app

Add a child issue and Create subtask only open the GIC modal when the issue type has Summary plus at least one other required field. Otherwise you get inline create and no UIM. Inline create on boards, backlogs and lists never runs it.

3. Dev mode

If forge tunnel is running, resolver output goes to the tunnel terminal and never reaches forge logs; the tunnelling docs say so directly. Tunnelling also only catches requests from the same machine and browser session.

4. Check that the entity is what you think it is

Call this with asApp():

GET /rest/api/3/uiModifications?expand=data,contexts

Then look at the contexts and isAvailable.

5. The first-option thing

onChange only fires on a change. Suppose the parent already has a value when the dialog opens, from a field default or one kept by Create another. Picking that value fires nothing, so nothing gets filtered and both child options stay visible.

Run the same filter in onInit from the parent’s current value, and register the child field there too:

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

uiModificationsApi.onInit(
  ({ api, uiModifications }) => {
    console.log('UIM onInit', uiModifications.length); // first line: proves the app loaded
    const parentValue = api.getFieldById('customfield_PARENT')?.getValue();
    applyChildFilter(api, parentValue); // the same function your onChange uses
  },
  () => ['customfield_PARENT', 'customfield_CHILD'], // your real field IDs
);

After that, onChange only has to handle real changes.

A quick split

The console.log on the first line of onInit tells you where to look:

  • Never prints (with Mihai’s console checks done) → it’s the context or the entry point.
  • Prints 1 → it’s the mapping, and logging parentOptionId will show it.

Hi @Mihai_leanzero, @RolfEriksen,

Thank you both, this was very helpful.

To answer the entry point question: I was testing from the Create button in the top nav, in two different projects (TPN and TPS).

I also went back through my own app code and found two problems on my side that match what you described:

  1. My debug logging was effectively silent. The UIM resource has no console.log calls at all, and the only observability was invoke() calls to a debug resolver. That function was registered on a different Resolver instance than the one the UI modification module points to, so every invoke failed. A .catch(() => {}) swallowed the error. So “no console output” and “no invoke reaching the resolver” were largely self-inflicted, and my original conclusion that the callbacks don’t run reliably was premature.

  2. onInit never applied the filter. Only onChange did. As Rolf described in point 5, if the parent already has a value when the dialog opens, selecting it fires nothing, so both child options stay visible. I’ve moved the filtering into one shared function that runs from onInit (using the parent’s current value) and from onChange, and I register both parent and child field IDs in both callbacks.

I’ve also added a console.log as the first line of onInit, as suggested. I’ll retest from the top-nav Create button in both projects, with the console set up the way Mihai described, and report back with whether onInit prints and what parentOptionId / cfg.mapping[parentOptionId] show.

Thanks again,
Vikas