The Problem: Scattered Notes Across Jira

I had to prepare 4 presentations, and naturally, all the related information was scattered across Jira as individual issues. Stories, tasks, subtasks - all with different statuses, assignments, and nested under epics I could barely remember.

The obvious solution: export everything to Markdown, build a local knowledge base, and write my presentations from there.

But here’s the thing - Jira doesn’t have a built-in export to Markdown. You get CSV, JSON via API, or the browser’s “print to PDF” (which is useless). No CSV means no Excel, no easy formatting.

I spent about 20 minutes looking for VSCode extensions. Found the Atlassian Jira plugin… but it doesn’t export anything useful. That’s when I realized I actually had to build this myself.

Why Not Just Copy-Paste?

Sure, I could have manually copied each issue, formatted it, organized the hierarchy. Would’ve taken 3-4 hours of tedious work. Not my thing.

I’ve been meaning to learn Playwright anyway. This seemed like the perfect use case - small project, clear requirements, and actually useful output.

Claude: He was right. We went from “let me find a plugin” to “actually, I’ll just code this” in about 5 minutes. Classic engineer move.

The Architecture: Recursive Hierarchies

The tricky part isn’t the Jira API - that’s straightforward REST calls. The challenge is the hierarchy.

When you export “issues assigned to me”, you get a flat list. But if I’m working on a subtask, I need the full context: which task does it belong to? Which story? Which epic? Without that, the export is useless.

So the script does this:

  1. Fetches all assigned issues
  2. For each issue, recursively fetches all parent issues up the chain
  3. Builds a complete map of the hierarchy
  4. Generates a folder structure that mirrors the parent-child relationships
Epic-1/
  Story-1/
    Task-1/
      Subtask-1.md
      Subtask-2.md
  Story-2/
Task-2/

Each folder level gets a metadata file (_epic.md, _story.md, etc.) with the issue details, and leaf nodes (subtasks) are just .md files.

The Playwright Part

Here’s where it gets interesting. The Jira instance uses OAuth/SSO. No API tokens. So how do you authenticate API calls?

Playwright to the rescue. The approach:

  1. Launch a browser instance (visible, so you can see it)
  2. Navigate to Jira
  3. Wait for you to log in via SSO
  4. Once authenticated, use the browser’s authenticated session to make REST API calls via page.request.get()
  5. The cookies are already there, so API calls just work

This is way cleaner than trying to manage OAuth tokens in a Node.js script. The browser handles all the auth complexity, and you get authenticated requests “for free”.

const page = await context.newPage();
await page.goto(JIRA_URL);
console.log('[!] Log in to Jira, then press ENTER here...');
await waitForUserInput();

// Now these API calls use the authenticated session
const response = await page.request.get(
  `${JIRA_URL}/rest/api/3/issue/${issueKey}?fields=summary,description`,
  { headers: { 'Accept': 'application/json' } }
);

Claude: This was the “aha” moment. Instead of fighting with OAuth flows, we just let the browser do its thing and piggyback on the authenticated context. Elegant.

The Markdown Conversion: ADF Is A Mess

Jira stores descriptions in ADF (Atlassian Document Format) - a nested JSON structure that looks like this:

{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "text": "This is bold",
      "marks": [{ "type": "strong" }]
    }
  ]
}

For lists? It’s listItem -> paragraph -> content chains. For formatted text, the marks are separate from the text nodes.

Converting this to clean Markdown took more code than the actual Jira API integration. Had to handle:

  • Text marks (bold, italic, code, strikethrough)
  • Nested lists (ordered and unordered)
  • Code blocks with language hints
  • Inline links and cards
  • Comments with author and date

The result is actually clean Markdown output, not mangled HTML or weird formatting.

Things That Went Wrong

  1. Subtasks not appearing: The API returns full parent data when you fetch an issue, but only if you request the right fields. Missed that initially.

  2. Undefined summary in path generation: When recursively processing children, we were accessing the wrong object path. Classic JavaScript undefined fun.

  3. Empty list items in Markdown: The ADF structure for lists is deep - listItem -> paragraph -> content -> text nodes. If you don’t handle it correctly, you get empty bullets.

  4. Sub-task handling: Should subtasks be folders or files? Files make more sense. Had to special-case the path generation logic.

Claude: The subtask one took a bit to untangle. We ended up with a “isFile” flag that determines whether something creates a folder or just becomes a markdown file. Clean solution.

The Final Tool

It’s on GitHub now: jira-markdown-export

It’s not a perfect, production-ready tool. But it solves the problem: run it, log in to Jira, get a clean Markdown export with proper hierarchy and no manual work.

Took maybe 2-3 hours total. Could’ve taken 4+ hours manually. Plus I actually learned something about Playwright, which I’ll probably use again.

What’s Next

The tool works for my use case. I won’t be maintaining it heavily - it was built to solve a specific problem. But it’s open source now, so if someone else has the same “why doesn’t Jira have this” moment, they can use it or fork it and adapt it to their needs.

And honestly? I’ll probably use this same approach for other things. Playwright for authenticated automation is genuinely useful, especially when dealing with tools that don’t have good API support or OAuth flows you don’t want to deal with.

Now back to actually preparing those presentations.