If you have ever tried to post a QA report to Jira Cloud with images that actually render inline, you know the feeling. You upload the screenshot. You reference it. And Jira shows you a gray box that says “External media” with a link, not the picture.
I spent hours on this. Not minutes. Hours.
Here is the part nobody tells you: it is not your fault, and it is not a bug. It is how Atlassian Document Format works, and once you see the mechanism, the whole thing becomes deterministic. No guessing. No LLM. Just bytes doing what bytes do.
This is Part 1 of two. Today: the problem, and the trick that solves it. Part 2: the full pipeline and the repo you can clone.
The problem, precisely
Jira Cloud stores rich content as ADF - Atlassian Document Format. It is JSON. A comment is a tree of nodes. Paragraphs, text, tables, and media.
To show an image inline, ADF needs a media node. That node does not point at your attachment. It points at a media UUID - an internal identifier for the image inside Atlassian’s media store.
And here is the trap. When you upload an attachment through the REST API, you get back an attachment id. You do not get the media UUID. Those are two different things. The attachment id is not enough to render inline. Most people find this out the hard way, give up, and paste a link instead.
So the real question is: how do you turn an attachment id into a media UUID?
The 303 trick
There is an endpoint:
GET /rest/api/3/attachment/content/{attachmentId}
Call it, and Jira does not hand you the file. It responds with a 303 See Other and a Location header. That Location URL contains the media UUID.
That is the whole secret. The UUID is sitting in a redirect you were never supposed to read.
Except most HTTP clients follow redirects automatically. Your fetch follows it, downloads the binary, and throws the Location header away before you ever see it. You get the file bytes. You do not get the UUID. And you have no idea why.
The fix is one flag:
fetch(url, { redirect: 'manual' })
Manual redirect handling. Do not follow. Read the Location header yourself. Pull the UUID out with a regex. Done.
The n8n footnote that cost me an afternoon
If you build this inside n8n like I did, there is a second trap on top of the first.
The modern helper this.helpers.httpRequest follows redirects and gives you no clean way to stop it in the 2.x sandbox. So it swallows the Location header, same as a naive fetch. You sit there staring at a 200 response wondering where your redirect went.
The older this.helpers.request lets you pass followRedirect: false. That is the one you want. An afternoon of my life, right there, in the difference between two helper methods.
I am telling you this so it costs you five minutes instead.
Why this matters beyond one image
This is the thesis of everything I build: context before LLM.
Resolving a media UUID is not a job for a model. It is a redirect, a header, and a regex. It is 100% deterministic. Same input, same output, every single time. If you throw an LLM at this, you are paying tokens to do something a byte parser does for free, and doing it less reliably.
The image belongs to the deterministic floor. The model - if you use one at all - decides what to write. The transport decides how it lands in Jira. Those are different layers, and mixing them is how you end up with slow, expensive, flaky reports.
In Part 2 I will show the full flow - create, upload, resolve, embed - the multi-stage builder pattern behind it, and the repo you can clone and run against a mock Jira in under a minute.
The code is public. Link in Part 2, and in the first comment.