Notes from the work

One email when a tutorial or migration write-up goes live. Nothing else, and one click to leave.

LeanZero

Two people in Romania doing Atlassian migrations, Forge apps and practical AI work for teams that would rather talk to the person doing the job. Most of what we learn ends up on this site.

Services

  • Atlassian Migrations
  • Atlassian FastShift
  • Atlassian Maintenance
  • Forge App Development
  • AI Development Consultation

Topics

  • Jira
  • Jira Service Management
  • Confluence
  • Bitbucket
  • Atlassian Forge
  • Cloud Migration
  • Local AI
  • AI Coding
  • Certifications
  • All topics

Company

  • Blog
  • Tutorials
  • Contact

Community

  • Join Discord
  • Support this site

© 2026 LeanZero. All rights reserved.

Privacy PolicyTerms of ServiceService Level AgreementTrust Center
  1. Home
  2. Portfolio
  3. Atlassian Asset Migrator
Migration Toolkit

Atlassian Asset Migrator

The scripts we used to migrate Jira Assets (Insight) from Data Center to Cloud — zero data loss.

View on GitHubRead the manual
DC to Cloud Zero Data Loss Resumable

The Challenge

Atlassian doesn't provide a turnkey migration path for Assets data. The documentation is sparse, the APIs are complex, and legacy Datacenter data brings its own surprises.

Undocumented Territory

Atlassian's Assets API documentation is notoriously difficult to decipher. Edge cases are everywhere, and the only way to figure things out is trial and error.

Tangled Dependencies

Assets data has deep relational structures—cross-schema references, circular dependencies, and objects that can't exist without each other.

Grueling Test Cycles

Every test run was long and tedious. Errors ranged from subtle data mismatches to hard API failures. There were no shortcuts—just persistence.

How It Works

A plan-driven architecture that pre-computes the entire execution order before touching Cloud. Every step is checkpointed for resumability.

1

Extract

Pull schemas, objects, attributes, references, and attachments from Datacenter via REST API.

2

Plan

Build a dependency graph, run topological sort, and detect circular references before creating anything.

3

Migrate

Create objects in Cloud in the correct order. Reference fields are resolved in a second pass after all objects exist.

4

Reconnect

Upload attachments and re-link Jira tickets to their migrated asset objects.

Built for the Hard Parts

The features that made the difference between a migration that sort-of-works and one that actually completes.

Circular Reference Handling

When Object A depends on B and B depends on A, neither can go first. A dedicated tracker defers these to a post-processing phase after all objects exist.

Resumable Operations

Migration state is checkpointed to disk. If it crashes at object 8,000 of 20,000, it picks up right where it left off—no duplicates, no data loss.

Multi-Token Parallelism

Distribute API calls across up to 5 tokens for higher throughput while respecting rate limits. Essential for large enterprise datasets.

Fuzzy Name Matching

Datacenter and Cloud don't always agree on naming. The schema mapper handles underscores, case differences, partial matches, and pipe patterns automatically.

Attachment Migration

File attachments are downloaded from Datacenter and re-uploaded to their corresponding Cloud objects with retry logic for transient failures.

Dry-Run Mode

Full validation pass without creating anything in Cloud. Catches attribute mismatches, missing references, and configuration errors before the real run.

The manual

Every step, in the order you actually run them: credentials and the two things people get wrong first, extracting from Data Center, the migration run itself, attachments and ticket connections, and what to do when it fails at object 8,000 of 20,000. 17 sections.

Written against the toolkit's own source — every flag is read from its configuration manager, every environment variable from its.env.example, and every utility from what is actually in the repository.

In this manual
  • What do I need before I start?
  • How do I get the token and the workspace id?
  • How do I get the data out of Data Center?
  • What is the correct run order?
  • What is the engine actually doing?
  • Which flags and settings actually matter?
  • What is in standalone-utilities, and when do I need it?
  • How do I migrate attachments without the run falling over?
  • get_datacenter — how do I run the extraction?
  • upload_attachment_assets — how do I run it, and why separately?
  • connect_assets_tickets — how do I run it, and how do I recover it?
  • sync_comment_visibility — how do I run it?
  • automation-service — how do I run it?
  • src_misc — what is in it, and when would I use it?
  • It crashed at object 8,000 of 20,000. What now?
  • What do the common failures actually mean?
  • How do I prove the migration actually landed?

01What do I need before I start?

Node 18 or newer, Assets enabled on the Cloud target, REST access to Data Center, and bash + curl + jq for the extraction scripts. Plus two credentials most people get wrong on the first try.

Requirements

WhatWhyCheck it
Node.js 18 or newerThe migration engine and every Node utility.node --version
Jira Cloud with Assets enabledThe target. Assets must already be provisioned — the toolkit migrates into a workspace, it cannot create one.Open Assets in the Cloud UI
Jira Data Center with REST accessThe source. Extraction is read-only.curl -u user:pass <dc>/rest/api/2/myself
bash, curl, jqThe Data Center extraction scripts are shell, not Node. macOS, Linux or WSL.jq --version
Assets admin on the Cloud targetCreating object types and objects requires it. A Jira admin who is not an Assets admin gets 403 several minutes into the run.Assets → Configuration
CarefulCheck the Assets admin permission before the first run, not during it. The engine authenticates successfully, builds the plan, starts creating, and only then hits 403 — so a permission problem looks like a migration problem for the first ten minutes.

02How do I get the token and the workspace id?

The token is base64 of email:api_token — not the raw token. The workspace id is a UUID you read from a REST endpoint in your browser.

Both credentials, in order

  1. 1Create an API token at id.atlassian.com/manage-profile/security/api-tokens.
  2. 2Base64-encode it with your email: echo -n "you@example.com:your-api-token" | base64. The result — not the raw token — is CLOUD_API_TOKEN. Missing the email and the colon is the single most common setup error, and it presents as a 401 that looks like an expired token.
  3. 3Read the workspace id by opening https://your-domain.atlassian.net/rest/servicedeskapi/assets/workspace in a browser where you are logged in. The workspaceId field is the UUID you need for WORKSPACE_ID.

The minimum viable .env

cd asset-migration-script
cp .env.example .env

CLOUD_BASE_URL=your-domain.atlassian.net    # no https://
CLOUD_API_TOKEN=<base64 of email:api_token>
WORKSPACE_ID=<uuid from the endpoint above>
DATACENTER_PATH=                            # defaults to ../datacenter_assets
NoteThere are two `.env` files, in different directories. asset-migration-script/.env drives the core migration and the ticket connector. standalone-utilities/.env drives src_misc and upload_attachment_assets. get_datacenter uses neither — you edit main/datacenter_common.sh directly — and automation-service takes CLI arguments only.

03How do I get the data out of Data Center?

Four shell scripts in standalone-utilities/get_datacenter, run behind the firewall. They write JSON into datacenter_assets/ at the project root, which is what the engine reads.

Extraction is deliberately shell rather than Node, because it usually has to run on a machine inside the network that can reach Data Center — often not the machine that will run the migration. Configure it once in main/datacenter_common.sh, then run whichever extracts you need.

The extraction scripts

ScriptExtractsNeeded for
get_datacenter_assets.shSchemas, object types, objects, attributes, referencesAlways. This is the core export.
get_datacenter_attachments.shObject attachmentsUPLOAD_ATTACHMENTS=true, or upload_attachment_assets later
get_datacenter_ticket_associations.shWhich tickets reference which objects, via custom fieldsCONNECT_TICKETS_TO_OBJECTS=true, or connect_assets_tickets later
get_datacenter_comment_visibility.shPer-comment visibility settingssync_comment_visibility later
generate_automation_mapping.shSource data for the automation rule mappingautomation-service

Run the core extract

cd standalone-utilities/get_datacenter
# Edit main/datacenter_common.sh with your DC base URL and credentials first.
bash main/get_datacenter_assets.sh

# -> creates ./datacenter_assets/ at the project root
TipKeep the export. Extraction is the slow part, and every subsequent dry run, retry and troubleshooting session reads it rather than Data Center. Copy datacenter_assets/ somewhere safe before you start migrating — re-extracting because you deleted it costs hours you did not budget.

04What is the correct run order?

Dry run, one schema, verify in the UI, then widen. Never the whole workspace on the first attempt — and never with attachments and tickets enabled before objects are proved.

The order that works

  1. 1`node main.js --dry-run` — validates the configuration and shows what would be created. Nothing is written. Read the log, not just the summary line.
  2. 2One schema, objects only. node main.js --schema "Hardware" --limit 25 with UPLOAD_ATTACHMENTS=false and CONNECT_TICKETS_TO_OBJECTS=false. Twenty-five objects is enough to expose an attribute-type mismatch and small enough to clean up by hand.
  3. 3Open Assets in the Cloud UI and look at those objects. Check the attributes, then check a reference actually points somewhere. A created object with an empty reference is the failure mode that survives every API-level check.
  4. 4The whole schema. Drop --limit. Re-verify a sample.
  5. 5Then attachments, then tickets — one at a time, each verified before the next is enabled. Combining them means a failure tells you nothing about which subsystem produced it.
  6. 6Then the remaining schemas, one at a time via --schema.
CarefulMigrate one schema at a time even when you do not have to. A failure inside a single schema is diagnosable; the same failure across a whole workspace is a log file nobody reads. SCHEMA_FILTER / --schema exists for exactly this.

05What is the engine actually doing?

Seven phases. The two that matter for correctness are dependency analysis, which decides creation order, and reference resolution, which runs after every object exists.

The seven phases

  1. 1Schema discovery — read the exported schemas and analyse their structure.
  2. 2Type mapping — map DC object types to Cloud-compatible shapes. Fuzzy name matching handles underscores, case differences, partial matches and pipe patterns, because the two instances rarely agree on naming exactly.
  3. 3Dependency analysis — build the dependency graph and topologically sort it, so an object is never created before something it references.
  4. 4Object creation — create objects in Cloud in that order, with their attribute values.
  5. 5Reference resolution — a second pass that fills in references, after every object exists. This is why circular references are survivable at all.
  6. 6Attachment upload — download from DC, upload to the created objects.
  7. 7Ticket connection — link Jira tickets to the migrated objects through their custom fields.
NoteCircular references get their own tracker. When object A depends on B and B depends on A, neither can go first — no topological order exists. Those pairs are deferred to the post-creation pass and resolved once both objects have Cloud ids. --clean-circular-refs, optionally with --remove-resolved, tidies the tracking files afterwards.

06Which flags and settings actually matter?

Every setting has both a CLI flag and an environment variable; the flag wins. These are the ones that change what the run does, rather than how fast it does it.

The settings that change behaviour

FlagEnv varDefaultWhat it does
--dry-runDRY_RUNfalseValidate and report; create nothing.
--schema <name>SCHEMA_FILTERallMigrate one schema. Use it.
--type <name>TYPE_FILTERallOne object type within the schema.
--limit <n>LIMIT_PER_TYPE0 (no limit)Cap objects per type — the smoke-test lever.
--upload-attachmentsUPLOAD_ATTACHMENTSfalseUpload attachments during the run.
--connect-ticketsCONNECT_TICKETS_TO_OBJECTSfalseLink tickets to objects during the run.
--auto-create-typesAUTO_CREATE_OBJECT_TYPEStrueCreate missing object types rather than failing.
--auto-create-refsAUTO_CREATE_REFERENCEStrueCreate missing referenced objects.
--validate-refsVALIDATE_CROSS_SCHEMA_REFStrueValidate cross-schema references.
--analyze-dcDATACENTER_ANALYSISfalseReport DC-versus-Cloud configuration differences. Run this before migrating.
--reportGENERATE_REPORTSfalseWrite the run reports.
--cleanup-objectsCLEANUP_OBJECTSfalseDeletes all objects from the Cloud workspace before migrating. Sandbox only.
CarefulLeave the four "NOT RECOMMENDED" settings alone. SKIP_VALIDATION_ERRORS, IGNORE_MISSING_REQUIRED and ALLOW_PARTIAL_MIGRATION each turn a loud failure into a quiet, half-migrated object. CLEANUP_OBJECTS=true empties the target workspace. All four exist for specific recovery situations; none of them belongs in a production .env.
TipThroughput knobs, in the order to reach for them: lower MAX_CONCURRENT_REQUESTS when you see 429s; raise BATCH_SIZE and ATTACHMENT_UPLOAD_WORKERS when you do not. MULTI_TOKEN_MODE with TOKEN_1 through TOKEN_5 spreads calls across up to five tokens for genuinely large workspaces — it is the difference between an overnight run and a three-day one.

07What is in standalone-utilities, and when do I need it?

Six utilities. Two do jobs the main engine can also do inline — run them separately when you want to retry that job alone, without re-running the migration.

The six utilities

UtilityPurposeConfigured by
get_datacenterExtract Assets, attachments, ticket associations and comment visibility from DC. Shell scripts, run behind the firewall.main/datacenter_common.sh
upload_attachment_assetsUpload attachments to objects that already exist in Cloud. The retry path when the inline upload failed for a subset.standalone-utilities/.env
connect_assets_ticketsLink Jira tickets to Assets objects that already exist. Same relationship to the inline ticket connector.asset-migration-script/.env
sync_comment_visibilitySync per-comment visibility from DC to Cloud. Two-step: a bash extract behind the firewall, then a Node updater with parallel workers.its own README
automation-serviceMigrate Jira automation rules between instances with updated object ids.CLI arguments only
src_miscAdmin helpers — bulk role assignment, object cleanup.standalone-utilities/.env
NoteAttachments and ticket connections exist in two places on purpose. Running them inline is fewer passes; running them standalone lets you retry just that job against objects that are already correct, without re-running object creation. On a large migration you will want both.

08How do I migrate attachments without the run falling over?

Extract them first, cap the size, and treat attachments as a separate pass once objects are proved correct.

The attachment pass

  1. 1bash main/get_datacenter_attachments.sh in get_datacenter — attachments have to be extracted before anything can upload them.
  2. 2Set MAX_ATTACHMENT_SIZE_MB (default 100) and MAX_ATTACHMENTS_PER_OBJECT (default 10) deliberately. The defaults are conservative; raise them knowingly.
  3. 3Run with --upload-attachments, or run upload_attachment_assets standalone against objects that already exist.
  4. 4Check a handful of objects in the Cloud UI. An object with a correct attachment count and zero downloadable files is a real outcome, and only the UI shows it.
LimitFiles over the Cloud size limit will not upload, and nothing can change that from the API. They are logged. Decide what happens to them — external storage, a link, or accepting the loss — as a documented decision rather than a silent one.

09get_datacenter — how do I run the extraction?

Five bash scripts, one shared config file, run behind the firewall. Only the first is always required; the rest are prerequisites for specific features.

Configure once, then run what you need

cd standalone-utilities/get_datacenter
# Edit main/datacenter_common.sh: DC base URL and credentials. No .env here.

bash main/get_datacenter_assets.sh                 # ALWAYS — the core export
bash main/get_datacenter_attachments.sh            # if UPLOAD_ATTACHMENTS=true
bash main/get_datacenter_ticket_associations.sh    # if CONNECT_TICKETS_TO_OBJECTS=true
bash main/get_datacenter_comment_visibility.sh     # for sync_comment_visibility
bash main/generate_automation_mapping.sh           # for automation-service
CarefulRunning the migration with `UPLOAD_ATTACHMENTS=true` and no attachment extract is a silent no-op. The engine looks for files that were never pulled, finds none, and reports success. Each extract is a prerequisite for the feature it feeds — not an optional extra.
TipExtraction is shell rather than Node on purpose. It usually has to run on a machine inside the network that can reach Data Center, which is often not the machine that will run the migration. bash, curl and jq are the only dependencies.

10upload_attachment_assets — how do I run it, and why separately?

The retry path. The main engine can upload attachments inline; this uploads them to objects that already exist, so a failed subset can be re-run without repeating object creation.

Diagnose, then upload

cd standalone-utilities/upload_attachment_assets
# Uses standalone-utilities/.env

node check_config.js                     # confirm credentials and workspace id
bash diagnose_attachment_upload.sh       # probe the endpoint end to end
bash find_valid_attachments.sh           # what is actually present in the extract
node upload_attachments_to_existing_objects.js --dry-run
node upload_attachments_to_existing_objects.js --confirm

Flags

FlagWhat it does
--dry-run / --confirmPreview, or write. Confirmation is required to upload.
--schema <name> / --type <name>Scope to one schema or object type.
--limit <n> / --batch-size <n>Cap and batch size.
--max-attachments <n> / --max-size <mb>Per-object cap and per-file size limit.
--workers <n> / --parallelUpload concurrency.
--rebuild-planDiscard the cached plan and rebuild it from the current extract and Cloud state.
Tip`test_attachment_endpoint.js` before a large run. Attachment upload is the part of the Assets API most likely to differ between tenants, and a single probe is far cheaper than discovering it 8,000 objects in.

11connect_assets_tickets — how do I run it, and how do I recover it?

Links Jira tickets to Assets objects that already exist. It ships more recovery tooling than any other utility here, because ticket connection is the step that most often half-finishes.

Run, then recover

cd standalone-utilities/connect_assets_tickets
# Uses asset-migration-script/.env

node index.js --dry-run --limit 50
node index.js --confirm

# When it half-finishes:
node overall_progress.js                       # where did it get to
node analyze_failures.js                       # why did rows fail
node analyze_field_errors.js                   # which custom fields rejected the write
node fix_stuck_processing.js                   # release rows wedged in "processing"
node reset_failed_ticket_connections.js        # flip failed rows back to pending

The recovery scripts

ScriptWhat it tells you
overall_progress.jsCounts by status across the whole plan. Start here.
analyze_failures.jsGroups failures by cause, so you can see whether it is one problem or fifty.
analyze_field_errors.jsSpecifically which ticket custom fields rejected the write — usually a type or context mismatch.
fix_stuck_processing.jsRows left in processing by a killed run are neither done nor retryable. This releases them.
reset_failed_ticket_connections.jsFlip failed back to pending. After fixing the cause, not instead of.
ticketConnectionPlanManager.jsThe plan store the others read and write.
CarefulRun `fix_stuck_processing.js` first after any killed run. Rows wedged in processing are skipped by both the executor and the retry path, so they quietly never complete and the totals never reconcile.

12sync_comment_visibility — how do I run it?

Two steps by design: a bash extract that runs behind the firewall, and a Node updater that can run anywhere.

Extract, then apply

# 1. Behind the firewall, against Data Center:
cd standalone-utilities/get_datacenter
bash main/get_datacenter_comment_visibility.sh

# 2. Anywhere, against Cloud:
cd ../sync_comment_visibility
node sync_comment_visibility_to_cloud.js --dry-run
node sync_comment_visibility_to_cloud.js
CarefulVerify in the customer portal, not the agent view. A comment that was internal on Data Center and lands public on Cloud is a disclosure. The agent view shows both kinds identically; only the portal shows what a customer actually sees.

13automation-service — how do I run it?

CLI arguments only, no .env. Migrates automation rules between instances with the object ids remapped.

Run it

cd standalone-utilities/automation-service

node index.js \
  --source-site-url source.atlassian.net --source-cloud-id <id> \
  --source-username you@example.com --source-token "$SRC" \
  --target-site-url target.atlassian.net --target-cloud-id <id> \
  --target-username you@example.com --target-token "$TGT" \
  --mapping-file mappings.json --rules-file rules.json

Flags

FlagWhat it does
--source-site-url / --source-cloud-id / --source-username / --source-tokenThe instance rules are read from.
--target-site-url / --target-cloud-id / --target-username / --target-tokenThe instance they are written to.
--site-url / --cloud-id / --username / --tokenSingle-instance shorthand, for in-place work.
--rules-file <path>The exported rules to process.
--mapping-file <path>Source-to-target object id map, generated by generate_automation_mapping.sh.
--default-account-id <id>Fallback actor when a rule's own actor cannot be resolved on the target.
--yesSkip the interactive confirmation.
NoteThe same actor-permission trap applies here as anywhere else in automation migration: a rule whose actor has no permissions on the target imports cleanly and then does nothing. Set --default-account-id to an account you know can act.

14src_misc — what is in it, and when would I use it?

Admin helpers for during and after a migration. cleanup_objects.js is the destructive one — read this before running it.

Object cleanup

cd standalone-utilities/src_misc
# Uses standalone-utilities/.env

node cleanup_objects.js --dry-run --schema "Hardware"
node cleanup_objects.js --schema "Hardware" --confirm
Careful`cleanup_objects.js` deletes Assets objects and there is no undo. Scope it with --schema, dry-run it, and read the count before confirming. The main engine's CLEANUP_OBJECTS=true does the same thing for the whole workspace — treat both as sandbox-only unless you are deliberately re-running a failed migration from zero.

The TEST_OBJECT_IDS environment variable scopes several of these helpers to a handful of known objects, which is the right way to prove a destructive helper behaves before pointing it at a schema.

15It crashed at object 8,000 of 20,000. What now?

Re-run it. Migration state is checkpointed in logs/migration_plan.json, and already-created objects are skipped — no duplicates.

The plan file is the source of truth for what has been done. Every object carries a status, and every created object's DC-to-Cloud id mapping is tracked separately in created_objects_mapping.json, which is what makes reference resolution possible across a restart.

The recovery scripts

ScriptWhat it does
analyze_failures.jsReads logs/migration_plan.json and reports every object with status failed, grouped so you can see whether it is one cause or fifty.
simple_analyze_failures.jsThe terser version of the same, for a quick count.
reset_failed_to_pending.jsFlips failed rows back to pending so the next run retries them. Run it after fixing the cause, not instead of.
check_cloud_attrs.jsDumps the Cloud attribute configuration for a schema from logs/cloud_configuration.json — the fastest way to see why an attribute write was rejected.
fix_mapping.jsRepairs created_objects_mapping.json when an object is marked created in the plan but never made it into the mapping.
CarefulNever reset failures to pending before you know why they failed. A retry against an unchanged cause produces the same failures plus wasted API budget, and the second run's log makes the first one harder to read.

16What do the common failures actually mean?

Four causes account for almost everything: the token encoding, the Assets admin permission, an attribute type mismatch, and a reference to something that does not exist yet.

Symptom to cause

401 immediately
The token is not base64 of email:api_token. Re-encode with echo -n — a trailing newline from plain echo also breaks it.
403 partway through the run
Authentication is fine; the account is not an Assets admin on the target workspace. Fix the permission, then reset the failed rows.
Object creation fails on one attribute
Type mismatch, or a required attribute with no value. check_cloud_attrs.js shows the Cloud side; the intelligent-migration mode converts what it safely can and truncates over-length text, but it will not invent a required value.
References not resolved
The referenced object was not created, or the referenced object type does not exist on Cloud. Check the creation status of the target first — an unresolved reference is usually a symptom of an earlier failure, not its own problem.
429 rate limited
Lower MAX_CONCURRENT_REQUESTS, wait, and consider MULTI_TOKEN_MODE. Retries use exponential backoff, but sustained over-concurrency outruns it.
Attachment upload fails
The file is missing from the extract, over the size limit, or the target object was never created. Check in that order.

17How do I prove the migration actually landed?

In the Assets UI, on objects you chose in advance — not from the run summary. An object created with empty references passes every API-level check there is.

The verification that counts

  1. 1Pick the objects before the run. Choose ten that matter — the deepest hierarchy, the one with cross-schema references, the one with attachments — and write down what each should contain. Deciding what to check afterwards means checking what is easy.
  2. 2Open them in the Assets UI. Every attribute, not the first one. Then click a reference and confirm it lands on the right object.
  3. 3Count per object type, DC against Cloud. A whole type missing is easy to overlook when the total looks plausible.
  4. 4Open a ticket that referenced an asset and confirm the custom field points at the migrated object, not at an empty value.
  5. 5Download an attachment. A correct attachment count with an unreadable file is a real and common outcome.
  6. 6Re-run with `--dry-run`. If it now reports nothing left to create, the plan agrees with reality. If it wants to create objects again, something is wrong with the mapping.
Careful"20,000 objects created" is a proxy metric. It is entirely compatible with 20,000 objects whose references are empty, which is the same as no migration at all for anyone who uses the data. Assume the run is broken until an object you chose in advance proves otherwise.
LimitWhat deliberately does not come across: historical audit logs and change history; user references, where account ids differ and need manual adjustment; Assets-specific automation triggers; and DC-only features with no Cloud equivalent. The history trade-off is intentional — it keeps the core data migrating cleanly instead of risking partial failures trying to move everything.

Honest Trade-offs

No migration is perfect. Here's what this toolkit prioritizes and what it intentionally leaves behind.

What You Keep

  • All asset objects with their full attribute data
  • Cross-schema and circular object references
  • File attachments on every object
  • Jira ticket-to-asset connections
  • Schema and object type hierarchies
  • Status fields, select values, and all attribute types

What Gets Left Behind

  • Historical audit logs and change history
  • User account mappings (require manual adjustment)
  • Complex Assets-specific automation triggers
  • Datacenter-only features without Cloud equivalents

The history trade-off is deliberate—it ensures the core asset data migrates with zero loss rather than risking partial failures trying to move everything.

Planning a Jira Assets Migration?

The toolkit is open source and free. If you need help running a migration or want to talk through the approach, reach out.

View on GitHubAsk on Discord