Custom fields for approval status, progress, percentage and pending approvers

Description

Context

Approval Path keeps approval state in an issue property and in JQL functions, but it does not expose native Jira fields. As a result users cannot place the approval status as a board column, use it in a filter, sort by it, or use simple JQL without knowing our custom functions. We want to provide clear, computed fields that show the approval state directly on the work item.

Goal

Add read-only Jira custom fields that present the approval state, ready to use on boards, in filters, sorting and JQL.

Scope, fields

Five fields, all read-only:

  • Approval Status: aggregate status of the approvals on the issue (In progress, Approved, Rejected, Incomplete, empty when none). Searchable in JQL, usable as a column and for sorting. JQL autocomplete offers the four values through searchSuggestions.

  • Approval Progress: progress shown as Step: {stepsDone}/{stepsCount}, matching the format used on the approvals list.

  • Approval Percentage: completion percentage instead of a step count. Stored as a number between 0 and 100 so it can be compared (>= 50), sorted numerically and aggregated in gadgets; a view.formatter.expression renders it as 50% wherever Jira displays the field, while CSV export keeps the raw number.

  • Pending Approvers: users awaiting a decision on the active steps, as a list of account ids (type: user, collection: list), so Jira renders them as real users and JQL treats them as a user field.

  • Pending Approver Groups & Emails: group names and external approver emails awaiting a decision, as a list of strings.

The originally proposed single Pending Approvers field was split in two, because a Forge custom field holds one data type: keeping account ids in a user field is what makes = currentUser() and user-aware sorting work, and group names and e-mail addresses cannot live in the same field.

Handling multiple approvals on one work item

Less common but required.

Approval Status and Approval Progress prefix each value with the approval name, entries separated by commas:

  • Approval Status: Security Review: In progress, Budget Sign-off: Approved

  • Approval Progress: Security Review: Step: 1/2, Budget Sign-off: Step: 2/2

When there is a single approval, the field shows the value only.

Approval Percentage cannot carry a per-approval breakdown, because it is a number field. It holds one value covering all approvals on the work item, weighted by steps: round(sum(stepsDone) * 100 / sum(stepsCount)). For a single approval this is identical to the percentage shown on the approvals list.

The two pending approver fields are collections, so they merge everyone still awaiting a decision across all approvals on the work item, without name prefixes.

Technical solution

jira:customField modules in the manifest, all with readOnly: true. Values are pushed into Jira with POST /rest/api/3/app/field/value rather than computed by a Forge value function: a value function must reference a hosted Forge function, and this app integrates through Forge Remote, so a value function would mean a Forge FaaS invocation proxying to our backend on every issue view.

Two write paths:

  • Instant push for actions taken in the work item UI — create, decide, delete, archive, reassign step. The push runs in the same request, after the transaction commits, so the values are in Jira before the response returns. It never fails the user action: failures are logged and the queued entry survives for the scheduled flush. Kill switch: ap.custom-field.instant-push-enabled.

  • Queue for every other source — workflow post-functions, e-mail decisions, the REST API, bulk actions. ApprovalConditionService.executeBatch, the single choke point for every approval state change, registers an after-commit enqueue into custom_field_push_queue, flushed once a minute. The flush claims work with a per-host cap so one busy tenant cannot occupy the whole batch, keeps global priority ordering so live changes are always served before backfill, and separates host-level failures (401/403/410 — defer the chunk and refresh the cached field ids) from work-item-level rejections (split and drop, bounded by a per-flush drop budget).

The number of Jira calls does not increase: the instant push replaces the scheduled push for that work item rather than adding to it.

Reads happen entirely against our own database, by reference id, with no REST calls to Jira and without extending the entity property:

  • Approval name, status and progress come from flat, indexed columns on the approval config record — a single select by reference id with no joins to the step tables.

  • Pending approvers are read from the step tables by id in one query covering users, groups, vote options and external e-mails.

  • Status is derived from the current database state, so it is not affected by a stale rejected marker after a relaunch.

Progress denormalization: steps_done and steps_count columns on the approval config, populated on the write path that already runs on every decision (the path object already carries the computed values). Percentage is derived from these columns (round(stepsDone * 100 / stepsCount), summed across approvals when there is more than one) without an extra column. The columns are not backfilled in SQL — a SQL reimplementation of the parallel-group logic diverges from StepProgress.of(), which merges only consecutive steps sharing a group number. Instead they start as NULL and are computed lazily by ApprovalProgressRepair using the same Java implementation the application uses.

The whole feature is gated by a global setting, approval_custom_fields_enabled, off by default. While it is off nothing is queued and no Jira call is made, so sites that do not use the fields pay nothing. Turning it on seeds the queue with the site's current approvals; turning it off clears the queue for that site.

The work item view is not refreshed after an action. The only Forge API for that, view.refresh(), reloads every app on the issue view — Atlassian confirms there is no way to opt out — which costs more than the fields being stale until the next page load.

The existing entity property stays unchanged.

Acceptance criteria

  • All five fields are available to add through the standard custom fields administration, once the global setting is enabled.

  • Values are correct for: a single approval, multiple approvals, parallel steps, and after relaunching a rejected approval (current status, not historical).

  • The multiple-approval format for Approval Status and Approval Progress matches the section above (name, value, commas).

  • Approval Progress uses the Step: x/y format consistent with the approvals list (same steps done and steps count values).

  • Approval Percentage is a number: for a single approval it matches the progress percentage computed for the approvals list, and for several approvals it is weighted by steps across all of them. It supports range comparison, numeric sorting and aggregation, and displays as a percentage.

  • Approval Status is filterable in JQL, usable as a column and for sorting. = Approved matches work items with a single approval; work items with several approvals need ~, which the field description states.

  • Pending Approvers shows users, Pending Approver Groups & Emails shows groups and external emails, with no Jira REST calls needed to determine the recipients.

  • The status and progress read is a single flat select by reference id with no joins to the step tables.

  • The steps_done and steps_count columns stay current after every step decision, and existing approvals are filled in by the repair path rather than by a SQL backfill.

  • Turning the global setting on backfills the site's existing approvals; turning it off stops all pushes and clears the queue.

  • The entity property and existing JQL functions are unchanged (no regression).

Known platform limitations

  • Jira Service Management portal: the fields work in the agent view, in queues and in JQL, but they are not rendered on the customer portal — Forge does not render read-only fields on JSM portal requests.

  • JSM create screens: unlike in standard Jira projects, a read-only Forge field can be added to a service project's internal create screen, and issue creation then fails with Field does not support update. These fields belong on view screens only.

  • Rendering: field values are rendered by Jira. Custom rendering (a progress bar, a status lozenge) is possible only through UI Kit, which this app does not use, and would apply to the issue view and JSM portal view only.