RFC-150: Jira Expression enhancements for rich text 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 constructive. Thanks!

Project Summary

This RFC proposes a new RichDocument type for Jira expressions. It helps users work with rich text fields like Description, Environment, paragraph custom fields, and comment bodies regardless of their format. It can be built on over time, adding utility functions that make these fields easier to work with in Jira expressions.

  • Publish: October 1, 2026
  • Discuss: October 5, 2026–October 19, 2026
  • Resolve: October 26, 2026

Problem

The suggestion JRACLOUD-93971 called for some kind of consistent handling of rich text fields in Jira expressions. This RFC proposes a way to provide it.

The challenge we have is that a rich text field can reach a Jira expression in several forms, depending on the experience or API that makes the request:

  • a wiki markup String (older create and transition screens, REST API v2),
  • an ADF document Map (the new create and transition experiences, REST API v3),
  • a RichText object (system fields such as issue.description), or
  • null, blank text, or an empty document if the field has never been filled in.

This is mostly a historical artefact that goes back to how ADF was introduced into the product. Changing the current behaviour is also very hard, as changes to existing types can break scripts that have already adapted to the behaviour. As such, the current guidance for customers has been to work around this by trying to use the RichText type.

let plainTextValue = value => typeof value == 'Map' ? new RichText(value).plainText : typeof value == 'RichText' ? value.plainText : value;

While this approach has worked, it isn’t straightforward:

  • Script authors must handle variations in shape, which makes working with these types difficult.
  • It implies to users that plainText is unformatted text, when it is often wiki text.
  • It doesn’t make working with the content any easier, making users do a lot of trim() and replace() calls to figure out whether the text is blank.
  • new RichText(map) can’t always construct the object successfully, and fails with an evaluation error on documents it can’t convert.

Proposed Solution

We propose a RichDocument type, created with RichDocument.of(value). It retains the value in its original format, validates it once, and converts it only when an expression requests another format.

We’re adding a new type rather than extending RichText for two reasons. Most rich text values never arrive as a RichText, and creating one from a document is a pain point today. A separate type lets us accept every format safely as an additive change without affecting existing expressions.

Initially, we’re going to keep this very simple and provide just a few utilities, as listed below.

Member Returns Behaviour
RichDocument.of(value) RichDocument Accepts an ADF document, a wiki String, a RichText object, or null. Treats null as an empty document. Produces an evaluation error for an invalid ADF document or a value that is not rich text. Does not convert the value.
isBlank() Boolean Whether the document is empty or contains only whitespace and line breaks. Use this for required-field validators: it gives the same answer in every format.
asWikiText() String Returns wiki markup, converting only if the document is in ADF format.
asAdfMap() Map Returns an ADF document, converting only if the document is in wiki markup format.

We imagine these being used like this:

let descriptionDoc = RichDocument.of(issue.description)
// Is the description filled in?
!descriptionDoc.isBlank()
// Give me a wiki version of the description
descriptionDoc.asWikiText()
// Has anyone left a non-blank comment?
issue.comments.some(c => !RichDocument.of(c.body).isBlank())
// Return 'Please describe the change.' when the field is blank, otherwise return true
RichDocument.of(issue.customfield_10797).isBlank() ? 'Please describe the change.' : true

What counts as blank?

Blank means the content is empty, null or contains only whitespace.

In an ADF document this gets muddied a bit. Blank means every top-level block is a paragraph containing only whitespace text and hard breaks. Any other node (a mention, emoji, media, table, heading or rule) counts as content, even if that node is itself empty.

What if the document is malformed?

RichDocument.of will reject invalid documents and values that aren’t rich text at runtime, and the error will be reported as an expression evaluation failure. Users can still choose to handle this if they wish, as shown below:

try { !RichDocument.of(issue.customfield_10797).isBlank() } catch { 'The field could not be read.' }

Can a RichDocument be returned?

RichDocument is intended to act as a helper for use within an expression, not a value to return. If an expression returns one, the evaluate API returns the string "RichDocument", just as it returns a signature for a function.

What happens with the RichText type?

On our end, we want to avoid breaking existing scripts. That’s why we’re going with a new type rather than adjusting the old one. As such, RichText, new RichText(map), and plainText remain unchanged; there is no plan to remove them for the time being. The workaround above will continue to work wherever it works today.

Asks

We would like feedback from app developers and admins who write Jira expressions for rich text fields. In particular:

  1. Would the definition of blank match what your validators and conditions mean by “filled in”? Would you count any node types differently?
  2. Are there functions or features that would be useful to add to RichDocument, and how would you imagine they would work?
  3. Are the names RichDocument, of, isBlank, asWikiText, and asAdfMap clear?

References

One topic that is not mentioned yet in the RFC is costs and limits. The first order question is how much does RichDocument and its methods cost in terms of steps and operations.

Then the second order question is how can devs use the APIs if the costs are dependent on the document sizes. For example, it would be useful for our app to be able to do something like:

issues.map(issue => [
   issue.key,
   RichDocument.of(issue.customfield_10797)
     .asWikiText()
     .substring(0, 50)
])

But that kind of thing can’t really work in practice: asWikiText does the whole work of converting the entire document before substring looks at it. So even if most issues have tiny field values, if just a few of them have huge documents they’ll exhaust the steps budget. There is no way for the dev to even check if the document is large or small without actually doing all the conversion work anyway. Which means that there is no way to do this over a batch of issues, because you can’t know in advance how big the batch can be.

The other option of making any RichDocument action count as an expensive operation also makes it impossible to use it on a group of issues, even if their rich text content is something trivial like a link or couple words with occasional bolding.

One interesting solution would be to provide a method like getWikiTextPrefix(maxLength); the implementation should be such that the cost of executing it is bounded by maxLength, not by the size of the document that is being processed.

(The function can also do semantically-valid prefixing, e.g. to avoid leaving incomplete wiki markup, like in the middle of some *bolded* word it would know it needs to add a * at the end.)

Other methods that I think would be useful would be:

  • plain text functions: asPlainText() and getPlainTextPrefix(maxLength)
  • size estimation functions: hard to specify, but something that can count/estimate plaintext characters, ADF nodes, and/or wiki markup elements without paying the full cost of conversion can be very useful. Something like countPlainTextChars and countAdfNodes. Perhaps even countPlainTextCharsUpTo(maxLength).
  • even getAdfPrefix(maxLength) would be useful, if there was a way to specify it cleanly and implement it cheaply.

Thanks for the reply. I didn’t quite get around to covering cost.

In terms of evaluation, these functions can be treated as steps: RichDocument.of and asWikiText would each be counted as a step. The field values are also capped, as there are limits on the size of paragraph fields.

In terms of the functions you mentioned:

  • asPlainText()
  • getPlainTextPrefix()
  • getWikiTextPrefix(maxLength)
  • getAdfPrefix(maxLength)
  • countPlainTextChars()
  • countPlainTextCharsUpTo(maxLength)

How and when would you imagine these being useful, and what questions are you really trying to ask of the document? I’m asking to help me visualise some use cases and to see whether there are better options.

Take asPlainText. Do you want the document stripped back to almost no formatting, or do you want a human-readable plain text version of it (which is a conversion to a different format)? Why do you convert it to plain text? Is it just to remove the formatting so you can do a contains call, for example?

Thanks for the answer. If they only count as single steps they’re relatively easy to use, if a bit surprising (Jira expressions also count 1+1 as a step, and I would have thought converting a large-ish ADF document to wiki text would be more expensive).

About asPlainText: Our app allows users to look at a group of issues (the result of a JQL query) and filter or compute statistics based on their fields (users pick which fields). So essentially the app will need to load several field values for a batch of issues (let’s say a few hundred, maybe thousands) and then display those values in places like dropdowns, table headers, and chart labels. We (the app and the users) in this case don’t care about any formatting, we just need plain text labels.

Normally users opt for simpler fields, like Select or Plain text. But on some occasions they had reason to use a rich text field — e.g. to combine a URL with a product number — and they asked us to support these. Without this RFC we couldn’t because handling ADF documents in bulk would be impossible. We can do it with asWikiText (by parsing it and removing the styles client-side), but if Jira could do it directly it’s save a lot of processing time.

About the getXyzPrefix functions: for the same functionality described above, we want to support statistics/grouping/filtering on short fields, but we can’t usually distinguish those in advance, so we need a way to handle cases where the user somehow selects a field with large values. We can do that with just substring (either in the expression itself or after we download the value), but that means Jira’s server will still perform the conversion work. The idea of the ...Prefix function is to allow Jira to stop processing as soon as it accumulates enough characters. They’re mostly useless if Jira’s implementation is “convert it all and then use substring”.

The countXyz functions are less common, mostly for something like “check quickly if the field is small or large” by looking at a lot of issues.

Actually, now that I thought about it more with my team, it might even be useful to have something like asHtmlString, though maybe that one is a bit trickier to implement properly.

Thanks for clarifying.

You’re right that the cost calculation is fairly simplistic at the moment. I deliberately didn’t change how steps are counted as part of this RFC. The new conversion is more expensive than something like 1 + 1, but it isn’t much more expensive than the existing new RichText().toPlainText() calls, which are already counted as steps.

We may revisit this in future.

On your suggestions:

  • asPlainText: We can do this, but you should aware that its output is closer to an indexable text format rather than being truly plain text (e.g. some formatting is applied to certain elements). It has quirks around media, mentions, links and a few other content types. Our view is that we can add the mechanism in this RFC with that initial implementation and address those quirks later.
  • asHtmlString and getXyzPrefix: We’d prefer to track interest in these as a feature request to see how much interest there is in these ones. For example, ADF-to-HTML conversion is still experimental and can’t represent everything (merged table cells, for one). The prefix conversion may provide some efficiencies, but there are some complexities to it that might make it better to add to the tooling independently.

Once this RFC is resolved, I’ll collate these suggestions into a single follow-up request so interest can be tracked.

Hello,
We are currently working on a Forge app where we generate Jira expressions for things like Field Required validators and run expressions from the backend for some JQL-related features, so definitely RichDocument looks useful to us. In terms of the feedback:

  1. Would the definition of blank match what your validators and conditions mean by “filled in”? Would you count any node types differently? - mostly yes, especially for the cases where we’re actually checking rich text content. For description/environment we do something like “field exists and plainText is truthy.” For paragraph custom fields we branch on typeof == 'Map' and either new RichText(map).plainText.length > 0 or treat it as a plain string and check .length. So that matches the pain you’re describing - awkward branching, and new RichText(map) can blow up on bad ADF. RichDocument.of(...).isBlank() is closer to what admins expect than what we generate now.
  2. Are there functions or features that would be useful to add to RichDocument, and how would you imagine they would work? - 2 small ideas that came to my mind that we could potentially use in our app:
  • matches / contains - we build things like issue.description.plainText.match(...), so a helper that works regardless of whether the value started as ADF or Wiki could be helpful,
  • equals / same content - for “compare field A to field B” conditions, because today we only unwrap RichText, not ADF Map, so field-to-field equality is unreliable.
    Also you mentioned that “RichDocument.of(value) accepts an ADF document, a wiki String, a RichText object, or null. Treats null as an empty document. Produces an evaluation error for an invalid ADF document or a value that is not rich text. Does not convert the value.” - not sure how it would work exactly under the hood but it would be nice if isBlank() stayed also cheap and not force format conversion on every issue (just a small concern because our app sometimes runs expressions over many issues from JQL, so I wouldn’t want this check to be slow).
  1. Are the names RichDocument, of, isBlank, asWikiText, and asAdfMap clear? - Yup, sounds clear to me :slight_smile:

@PiotrBuczekAppfire

Thanks, this is really useful, and it’s good to hear RichDocument fits your validator and JQL use cases.

  1. Blank checks. isBlank() treats a document as blank if it’s empty or holds only whitespace, line breaks and empty paragraphs. That’s a little different from what the field required validator does today, which only accepts a handful of exact empty shapes, so some whitespace-only content currently counts as filled in. We may revisit that check in the validator so the two behave the same way. The alternative would be to also offer an isEmpty check here as well that gives results closer to the current validation.

You also said it would be nice if isBlank() stayed cheap and didn’t force format conversion on every work item. That’s a good point. We may be able to return early rather than convert fully, and at the very least memoise any converted value so later calls stay cheap.

  1. matches / contains and equals. A helper that works the same whether the value started as ADF or wiki, and an equals for field-to-field comparison, are things we’d like to track as feature requests rather than add in this RFC. There are challenges in getting the semantics right, and they may need their own RFC or design.

For example, one likely reality is that the format a value arrived in affects what you’d need to compare against, so callers may need to be aware of it. Even visually identical content can differ structurally, for example doc(p("Hello, world")) and doc(p("Hel", "lo, world")).

It would help to know what you’d use the methods, whether you need to see links, mentions or attachments, or only the visible words for within the matching.

  1. Names. Glad they read clearly.

Thanks for the follow-up!

For our app, the main value from this RFC is isBlank() for required-field-style checks, so we’re good on that. If you add matches / equals later: for our JQL regex and guided “field equals field” rules we would want visible text (de-marked-up), not wiki source or structural ADF equality. Mentions/links as display text is usually enough. It’s totally fine if those stay separate feature requests.

Mentions/links as display text is usually enough. It’s totally fine if those stay separate feature requests.

The fun thing is the plainText returned by RichText like on the description field is that it is actually the converted wiki text.

Our current internal implementation of plainText, (the one I am looking at exposing), strips a lot of the elements so the information can be indexed without complex elements impacting on the search. There is PII/Privacy setting implications around mentions. So we may need to build something more specific for the RichDocument.

Some of the problem I’m looking at in terms of plain text are:

  • A link is stripped of it’s url to only show the display name. Should the URL always be there. What about alt text?
  • A mention gets turned into an accountId if it can ← and recall that there’s all sorts of fun privacy controls around accounts. Do we need functions to allow proper resolution to names? Does that create a new cost.
  • An attachment may be media Id, file name may not be visible and the file may not yet be saved during transition time. Do we return mediaId, ‘display names’ etc… If we want extra details is that an extra cost function to the conversion?
  • If an app has a doc extension, the information around that ends up being stripped out and visible text in it may not be visible. We think we should strip these extensions out completely.
  • Structural elements are normalised/stripped during the process of turning it into plain text. Are certain structural elements still interesting to users for example in the plainText? Perhaps they should use wiki or just the Doc if they do care there.

So when we talk about ‘visible text’ it gets a bit more interesting as a definition for a general purpose function.