u/CherryTraditional999

▲ 1 r/jira

We built our own YouTrack → Jira migration engine with Claude instead of hiring consultants. Here's what actually broke.

TL;DR: We migrated 2,900+ YouTrack issues to Jira ourselves using a custom Python engine built with Claude. A two-week POC saved us — it caught ADF failures, type mapping gaps, and relationship bugs before they could touch production. The main migration ran over a weekend. Things still went wrong. This post is the honest account of what they were and how we fixed them.

We built a custom Python migration engine entirely with Claude — no consultants, no professional services engagement. Just AI, a mapping file, and a lot of edge cases.

I'm writing this because when I was planning it, there was almost nothing useful out there. Most of what I found said: use Jira's built-in import tool. That works for simple cases. Ours wasn't simple. This is the honest account I wish I'd had.

The real problem

YouTrack wasn't broken. It had just quietly become its own source of friction — workflows too complex, custom fields multiplied past usefulness, no clean picture across teams. Moving to Jira wasn't the goal. Resetting was.

The scope

  • 2,900+ issues across multiple source spaces in YouTrack, landing across multiple Jira projects
  • Several source-to-destination project mappings, some consolidating multiple YouTrack spaces into a single Jira project
  • Everything had to come across: descriptions, comments, work logs, attachments, relationships (parent-child, blocks, implements, relates), custom fields, story points, and resolution states
  • Zero tolerance for duplicates — we needed to run the migration incrementally without re-creating issues that already existed

Pre-migration: metadata analysis first

Before writing a single line of migration code, we did a full metadata analysis across every YouTrack project — issue types, custom fields, relationship types, states, priorities, and hierarchy. Understanding the shape of the data before touching it is what made each project's migration as clean as possible. Skipping this step means rework.

The approach: incremental migration with a mapping file

The first thing we built was a mapping file — a JSON file tracking YouTrackID → JiraID for every migrated issue. The design: before creating anything, check the mapping. If it's there, skip it. The goal was to run the migration as many times as needed without duplicates, migrating in batches rather than a big-bang cutover. Getting that truly idempotent took more iteration than expected — edge cases in how the mapping was being written let some issues slip through. More on that in item 8.

We also built a --dry-run flag from the start. Every run prints what would happen without touching anything. This saved us several times. All Jira updates used notifyUsers=false to avoid spamming the team.

What went wrong (the useful part)

Before the main migration, we ran a full POC on a single project — two weeks out, deliberately, to stress-test the engine in a lower-stakes environment. That decision is what made the main migration as clean as it was. Most of the problems below surfaced in the POC and were fixed before the main run. A few didn't show up until later. I've noted which is which.

  1. Atlassian Document Format (ADF) is not Markdown

(Caught in POC) Jira's API doesn't accept Markdown. It expects ADF — a proprietary JSON structure where every paragraph, heading, bullet, and inline code span is a nested node. We had to write a markdown_to_adf() converter from scratch. The POC revealed it fell apart for edge cases. ADF and formatting problems were the single biggest source of rework throughout the migration — more than any other category, and the reason the POC approach proved so valuable.

  1. Inline code spans were eating angle brackets

(Caught in POC) YouTrack descriptions used backtick inline code spans like \<some_value>``. Our converter ran HTML unescaping on content before parsing backtick spans — so the angle brackets were being stripped as HTML tags, leaving an empty string. The fix: a protect/restore pattern that replaces angle brackets inside backtick spans with null-byte placeholders before HTML processing, then restores them after ADF conversion. Fixed before the main migration ran.

  1. The merged-text bug

(Caught in POC) For some issues, the entire description — headings, bullets, tables, everything — collapsed into a single ADF paragraph node with raw Markdown as literal text. One giant wall of syntax characters in Jira. Root cause: an edge case where the converter failed to split content into blocks and fell back to dumping everything into one node. 25 POC issues identified, converter patched, main migration ran clean.

  1. Issue type mapping was wrong in the early batches

(Caught in POC) YouTrack has Feature, Enhancement, User Story, Documentation. Jira has Epic, Story, Task, Bug. We built a type mapping, but a frequency audit of POC output showed gaps — Feature issues landing as Story instead of Epic, Enhancement silently defaulting to Task. The 16 wrongly typed issues were in the POC project and corrected before the full migration ran.

  1. Features required special hierarchy considerations

Our Jira instance doesn't have the same hierarchy as YouTrack. In YouTrack, Features sit above Epics and can span multiple projects — Jira has no native equivalent. This required project-by-project decisions about how Features should be represented in each destination, and in some cases custom field workarounds to preserve hierarchy context.

  1. Blocking relationships arrived reversed

(Surfaced post-main-migration) YouTrack stores link relationships with an explicit direction per issue — INWARD (this issue is blocked by) vs. OUTWARD (this issue blocks). Our initial script read the link type name without checking direction. Result: "Issue A blocks Issue B" could arrive in Jira as "Issue B blocks Issue A." The relationship existed; the meaning was flipped. The fix required rebuilding the link migration logic with full direction-awareness and running a retrospective fix pass across all previously migrated issues. Don't assume the link type name tells you which way the relationship points.

  1. Cross-project relationships

Parent-child links in Jira are intra-project only. But in YouTrack, a Feature in one project could have child Epics in three other projects. Our solution: cross-project parent-child relationships become Relates to issue links. We built logic to detect when a relationship crossed project boundaries and handle it differently — including a sequencing fix, since children migrated before their parent won't get the parent link set automatically. The script re-runs relationship migration on every execution.

  1. Duplicate issues from re-runs

During one source space migration, issues were created more than once before idempotency was fully hardened. The mapping file check was in place, but an edge case in how the mapping was being written let some issues slip through. Identifying, cross-referencing, and cleaning up duplicates was tedious. "Idempotent" needs to be verified end-to-end, not just assumed. And for issues relevant to multiple destination projects: establish a hard rule — once an issue is migrated, it does not get a duplicate in another project.

What we'd do differently

The single most important thing: run a POC on one project before migrating everything. We did — two weeks out — and it changed the shape of the main migration entirely. Most of the issues above were caught and fixed there, not in production.

  • Build the ADF converter test suite against edge cases, not just happy paths. Tables, nested bullets, inline code with special characters, headings at line one — the POC revealed all of these. Build that corpus before your first real run.
  • Do a frequency audit of every issue type before writing the mapping. A five-minute count across all projects shows you exactly where the gaps are before they land in Jira at scale.
  • Automate the formatting verification check and run it with every batch. Build it in the POC, automate it, and it's just part of the pipeline by the time the main run happens.
  • Map and validate every link direction before migrating. Don't assume the link type name tells you which way the relationship points. INWARD and OUTWARD are not the same thing, and getting it wrong means all your blocking relationships are backwards.
  • Migrate relationships in a separate dedicated pass. Bolting it onto the main script creates ordering dependencies that are much cleaner as a standalone step.
  • Plan a dedicated post-migration pass for cross-project relationship reconciliation. Once all projects are live in Jira, run a targeted pass to verify and update all cross-project relationships. Easy to deprioritize; painful to clean up later.

The cleanup that came with it

The migration was also an opportunity to undo years of accumulated tooling debt. We intentionally chose not to port complexity — and the numbers reflect that:

  • Issue types: 7 → 4 (43% reduction — types folded, consolidated, and relabeled to match how work actually gets done)
  • Priority values: 10 → 5 (overlapping and rarely-used values rationalized to 5 clearly-named Jira priorities)
  • Workflow statuses: 8 → 6 (ambiguous terminal states collapsed into Done with resolution values set)
  • Workflow configurations: 20+ → 2 — and this number was inflated for a specific reason: YouTrack had been used to automate build pipeline triggers and release creation directly within its workflow engine. Deployment-specific logic was embedded in the same state machines as issue tracking, duplicating effort and making the workflow landscape far harder to maintain than it should have been. We chose not to migrate any of that. Build pipeline and release automation now live in the CI/CD pipeline where they belong. All 7 projects run on one of two shared schemes — Scrum or Kanban.
  • Labels: unmanaged tag set → 6 operational labels (everything else was noise and deliberately left behind)

The outcome

Everything made it across. 2,900+ issues, all relationships intact, comments, work logs, attachments, custom fields — clean. The team is fully off YouTrack. The incremental engine means we can still run it for any stragglers without touching what's already there.

And this is only the start

This was our first major project built entirely with Claude. Since then it's become a new way of working — if we need a capability, we build it. Timesheet reporting instead of Tempo. Incident mobilization. Discovery analysis. The question is no longer whether it's possible. It's how fast.

Happy to answer questions

If you're planning a similar migration and want to talk through the ADF converter, the mapping strategy, or the relationship handling - ask away. I have opinions.

reddit.com
u/CherryTraditional999 — 17 hours ago