Stay Updated

New tutorials, tips, and Atlassian insights. No spam, unsubscribe anytime.

L
LeanZero

An approachable expert helping teams simplify their Atlassian ecosystems. Sharing knowledge and building community, one solution at a time.

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
  • All topics

Company

  • Blog
  • Tutorials
  • Contact

Community

  • Join Discord
  • Support this site

© 2026 LeanZero. All rights reserved.

Privacy Policy|Terms of Service|Service Level Agreement|Trust Center
LZ·/PORTFOLIO·REV 2.6
  1. Home
  2. Portfolio
  3. Jira Workflow Automation Toolkit
Open-source migration toolkit

Jira Workflow & Automation Toolkit

Four Node.js tools for the part of a Jira migration no assistant handles — workflow rules, app workflow extensions, automation rules, and the JQL inside saved filters.

View on GitHubRead the manual
DC to Cloud and Cloud to Cloud Validate before mutate Operator review workbook Apache-2.0

What silently breaks

Issues migrate. The logic around them does not — and unlike missing data, broken logic does not look broken. A validator that no longer validates is invisible until someone submits bad data.

Rules that reference Java classes

A Data Center validator from a workflow-extensions app is a Java class name. Cloud has no Java. Unless the rule is re-expressed as a native Cloud rule or the Cloud app's equivalent, it silently does nothing at all.

Every entity id changes

Statuses, issue types, screens, events, roles, groups, priorities, resolutions, link types, security levels, custom fields. A workflow copied verbatim between instances is a workflow full of dangling references.

Filters that answer a different question

Saved filters are copied body-for-body, so their JQL still names DC filter ids, DC field ids and functions that do not exist on Cloud. The filter loads without error and returns plausible, wrong results — for months.

What is in the box

Four tools, run in a specific order: filters first because everything downstream is easier to verify with working JQL, then workflows, then the app rules inside them, then automation last because it depends on all three.

rewrite_filter_refs

1. Saved-filter JQL cleanup

Rewrites DC filter ids, cf[N] and customfield_N references, Assets fields and ORDER BY clauses; strips functions that do not exist on Cloud; validates project names; detects renamed priorities; and repairs share permissions with verified, retried POSTs.

clone_workflow_rules

2. Workflow collection, transformation and apply

Between instances or in place. Cross-instance remapping of every entity id. Cloud-to-cloud mode reads the new-format workflow endpoint directly, skipping the conversion step that is the biggest unknown elsewhere. Can also emit a ScriptRunner scaffold for handover.

migrate_jsu_rules

3. App workflow extensions to Cloud rules

Reads OSWorkflow XML exported from DC, identifies app rules by Java class name, and translates each into a native Cloud rule or the Cloud app equivalent — updating the same-named workflow in place. Every non-matching rule is left completely untouched.

automation_rules_migrator

4. Automation rules between Cloud sites

Export, generate id mappings, ensure the rule actor actually has the agent role the automation engine demands, then import with an explicit actor override. Plus a reconcile-in-place mode that repairs and enables existing rules without importing duplicates.

How every tool in this repository behaves

The same operating model throughout, deliberately — so that knowing one tool means knowing all of them.

Validate before mutate

Every workflow write is checked against Jira's own /workflows/update/validation endpoint first, and validation errors are surfaced for review rather than auto-fixed — an auto-fixed validation error is a change nobody decided on. Automation imports run PLAN=1 first.

Always-fresh deduplication

Every apply re-fetches the live target and fingerprints its existing rules before mutating. Anything already present is classified as such and never appended. A plan built last week cannot double-apply today.

Additive by default

These tools add and correct rules. They do not delete rules they did not recognise. The one exception is explicit and reported: rules whose custom-field references do not exist on Cloud are removed, because they are already broken.

An operator workbook, not just a log

Runs emit a manual_review_<timestamp>.xlsx with one tab per category needing human attention. It is a checklist, not a sign-off — a translated rule still deserves an expert eye in the Cloud UI.

Start here

git clone https://github.com/leanzero-srl/leanzero-jira-workflow-automation-toolkit.git
cd leanzero-jira-workflow-automation-toolkit/clone_workflow_rules
npm install
cp .env.example .env

# Take the backup FIRST — there is no rollback.
node main/clone_workflow_rules.js --collect

# Cloud to cloud: read the new-format endpoint, skip the conversion table.
node main/clone_workflow_rules.js --cloud-to-cloud --validate-only

The manual

Every question this repository raises, answered in order: what each tool reads and the order to run them in, how workflows and app rules are translated, the automation pipeline that actually works, and why a saved filter can return the wrong answer without ever erroring. 19 sections.

Contents
Set up and sequencing
  • 01What do I need, and what does each tool read?
  • 02What order should these run in?
  • 03How do I avoid breaking a workflow that currently works?
Workflows and app rules
  • 04How do I move workflows between Cloud instances?
  • 05My DC workflows are full of app validators. Cloud has no Java. Now what?
Automation rules
  • 06What is the reliable way to move automation rules between sites?
  • 07The rules are already on the target but wrong. Do I have to re-import?
Saved filters and JQL
  • 08My saved filters return the wrong results after migration. Why?
  • 09The share permissions did not stick. What is going on?
  • 10How do I prove any of this worked?
The four tools, one at a time
  • 11rewrite_filter_refs — how do I run it?
  • 12clone_workflow_rules — how do I run it?
  • 13migrate_jsu_rules — how do I run it?
  • 14automation_rules_migrator — how do I run it?
When it goes wrong
  • 15Automation rules import, then do nothing.
  • 16Workflow validation fails and I cannot tell why.
  • 17I re-ran it and now there are duplicate rules.
  • 18A rewritten filter returns the wrong results, or the shares vanished.
  • 19How do I prove any of this worked?

01What do I need, and what does each tool read?

Node 18 or newer and a Cloud API token. Two of the four tools also need Data Center — one over REST, one from exported workflow XML, because DC's REST API does not expose workflow rule bodies at all.

What each tool reads

ToolSourceTarget
clone_workflow_rulesJira Cloud REST (source instance)Jira Cloud REST (target, or the same instance in place)
migrate_jsu_rulesOSWorkflow XML exported from DCJira Cloud REST
automation_rules_migratorJira Cloud automation REST (source site)Jira Cloud automation REST (target site)
rewrite_filter_refsJira Cloud filters, plus DC REST to resolve filter ids to namesJira Cloud filters
CarefulData Center's REST API does not return workflow rule bodies. That is why migrate_jsu_rules reads exported OSWorkflow XML rather than calling DC — it is not a design preference, it is the only way to see the rules. Export the workflows from the DC admin UI first.
Notemigrate_jsu_rules reuses clone_workflow_rules's REST client and field mapper by relative path. Keep the two directories as siblings; do not move either one.

02What order should these run in?

Filters, then workflows, then the app rules inside those workflows, then automation last — because automation depends on the workflows and fields already existing.

The sequence

  1. 1`rewrite_filter_refs` — filters first. Everything downstream is easier to verify when the JQL you are verifying with actually works.
  2. 2`clone_workflow_rules` — get the workflows into place, either as in-place fixes on one instance or as a cross-instance clone.
  3. 3`migrate_jsu_rules` — then the app-specific rules inside those workflows.
  4. 4`automation_rules_migrator` — automation last. Rules reference projects, fields, statuses and workflows; importing them before those exist produces a long list of failures that tell you nothing.

03How do I avoid breaking a workflow that currently works?

Every workflow write is validated against Jira's own validation endpoint before it is sent, and every apply re-fingerprints the live target first so a stale plan cannot double-apply.

The three guards

Pre-flight validation
Runs against /workflows/update/validation before any mutation. Errors are surfaced for operator review, never auto-fixed — an auto-fixed validation error is a change nobody decided on.
Always-fresh deduplication
Every apply re-fetches each Cloud workflow live and snapshots its rule fingerprints BEFORE mutating. Rules already present are classified as such and never appended. A plan built last week cannot duplicate today.
Additive by default
These tools add and correct. They do not delete rules they did not recognise. The one exception is explicit and reported: migrate_jsu_rules removes pre-existing rules whose custom field references do not exist on Cloud, because those are already broken.
CarefulThere is no rollback. Re-running with prior state is the only undo. Take a full export before you start — clone_workflow_rules --collect for workflows, export_all.js for automation — and keep it off the machine running the migration.

04How do I move workflows between Cloud instances?

clone_workflow_rules --cloud-to-cloud. It reads the new-format workflow endpoint directly, which skips the single biggest source of uncertainty in the whole repository.

There are two ways to read a Cloud workflow, and the difference matters more than it looks. The legacy GET /rest/api/3/workflow/search returns the old rule-`type` format, which then has to be converted to Cloud's new rule keys at apply time. That conversion table is the biggest unknown in the tool.

For cloud-to-cloud the conversion is unnecessary. --cloud-to-cloud (alias --cc) reads via POST /rest/api/3/workflows — getWorkflowsEnvelopeByNames — which already returns every transition in the new format. Nothing is converted, so nothing can be converted wrongly.

The three modes, in order of maturity

ModeFlagsUse it for
In-place configuration fixes--apply --updateWorkflows already on a Cloud instance that need app Connect-prefix normalisation, post-function UUID cleanup and custom-field remapping after a migration.
Clone to another Cloud instance--apply (default)Replicating workflow configuration across orgs. Creates _v2 copies so you can cut over by reassigning the workflow scheme rather than mutating live workflows.
ScriptRunner handover--collect --export-scriptrunner-scaffoldEmits an extensions.yaml plus Groovy stubs compatible with the vendor's Dev & Deployment tool, for handing scripted rules to a team that deploys them properly.

What gets remapped across instances

  • statuses, issue types, screens, events
  • roles, groups, priorities, resolutions
  • issue link types, security levels
  • custom fields — matched by name, because ids do not survive
TipCreating _v2 copies rather than mutating the live workflow is deliberate. It makes the cutover a workflow-scheme reassignment, which is instant, visible and trivially reversible — instead of an in-place edit that is neither.

05My DC workflows are full of app validators. Cloud has no Java. Now what?

migrate_jsu_rules identifies each rule by its Java class name and translates it into either a native Cloud rule (system:*) or the Cloud app's equivalent (connect:*), then updates the same-named Cloud workflow in place.

What it does and does not touch

DoesDoes not
Reads OSWorkflow XML exported from Jira DCRead DC workflows over REST — DC does not expose rule bodies
Identifies app rules by their Java class nameTouch any non-matching rule: out-of-the-box, other vendors, scripted rules are left completely intact
Translates DC custom field ids to Cloud ids by exact name matchTranslate field values — status names and option labels pass through verbatim, which is usually right because Cloud uses names too
Updates the same-named Cloud workflow in placeCreate new workflows or rename existing ones
Removes pre-existing rules whose customfield refs do not exist on Cloud, and reports each oneTouch Connect or Forge rule config blobs, which are opaque stringified JSON
Validates against /workflows/update/validation before mutatingAuto-fix validation errors — they are surfaced for operator review
Fans out to all same-named Cloud transitions when DC common actions produce duplicatesDisambiguate transitions by from-status or to-status; only the name is used
CarefulTransitions are matched by name only. If a DC common action produced three transitions that share a name, the rule is applied to all three. That is the correct behaviour for a common action and the wrong behaviour if two unrelated transitions happen to be called "Approve". Check the workbook.
NoteEvery run emits `manual_review_<timestamp>.xlsx` — one tab per category needing human attention, produced on --apply, --validate-only and --dry-run alike. It is a checklist, not a sign-off: a translated rule still deserves an expert eye in the Cloud UI before anyone calls the workflow migrated.

06What is the reliable way to move automation rules between sites?

The standalone scripts, in order, not the interactive modes. Export, generate mappings, fix the actor's access, then import with an explicit actor override.

The pipeline that actually works

  1. 1`export_all.js` — full export of the source rules: a summary plus each rule body. This is also your backup. Keep it.
  2. 2`gen_mappings.js` — cache the source→target id maps (OUT=mappings.json). Projects, fields, statuses, priorities, link types.
  3. 3`ensure_actor_access.js` — add the rule actor to each target project's Service Desk Team (agent) role. Required, or JSM rules fail to create.
  4. 4`import_clean.js` — import the cleanly-mappable rules. Set ACTOR_OVERRIDE=<target accountId>. Honours PLAN=1 for a dry preview, and auto-discovers and remaps the Assets workspace id.
CarefulBeing a site admin is not enough, and `/mypermissions` lies about it. Without real agent membership, JSM rule creation fails with 400 component.missing.permissions.actor — while the permissions endpoint cheerfully reports that the account has the permission. The automation engine checks role membership, not the permission grant. This one costs an afternoon if you trust the API.
TipAlways set `ACTOR_OVERRIDE`. The source rule's actor is very often an app account with no permissions whatsoever on the target site. Rules created under it import successfully and then fail silently at run time, which is the worst of both outcomes.

07The rules are already on the target but wrong. Do I have to re-import?

No. reconcile_target.js fixes and enables rules in place — no import, no duplicates, no new rule ids.

This is the common case after a migration assistant has already carried the rules across: the rules exist, but they point at (migrated) field duplicates, stale ids or a disabled state. Re-importing would create a second copy of every rule; reconciling repairs the ones that are there.

What reconcile mode does

  • Repoints field references from (migrated) duplicates to the real fields
  • Fixes stale project, status and priority ids using the generated mappings
  • Enables rules that arrived disabled
  • Writes a full backup of every rule before changing anything
Noteensure_addon_access.js is the sibling for the other half of this problem: a rule whose actions call an app the target site's rule actor cannot reach. Run it when imports succeed and the rules then do nothing.

08My saved filters return the wrong results after migration. Why?

The migration copies filter bodies verbatim, so the JQL still names DC-era identifiers. The filter loads without error and answers a different question.

This is the quietest of all post-migration defects, because a filter that returns the wrong rows looks exactly like a filter that returns the right rows. Nobody reports it. It just feeds a dashboard that is subtly wrong for months.

The four reference classes it fixes

ClassWhat it rewrites
Filter referencesfilter = 12345 pointing at DC filter ids, resolved to names and re-pointed at the Cloud filter.
Custom fieldscf[N] and the customfield_N long form, remapped via an auto-built DC→Cloud field map.
Assets fieldsDirect asset-field rewrites, plus ORDER BY clauses that name asset fields.
Broken functions and valuesFunctions that do not exist on Cloud are stripped; project names are validated; unresolvable values are removed reactively rather than left to fail.
NoteIt also rewrites priority names proactively: it detects priorities renamed on Cloud and rewrites priority = … accordingly, which is a rename that silently empties a filter.

09The share permissions did not stick. What is going on?

Adding a group to a filter's share permissions only persists if the filter's owner is itself capable of sharing with that group. The tool swaps the owner to edit the JQL and then restores it — which silently drops the share.

Editing another user's filter requires owning it, so the tool swaps the owner, edits, and restores the original owner. Cloud then drops any org-admins share the restored owner is not entitled to grant — silently, with a 2xx on the original POST.

Two things were added because of this

Verified share POSTs
The share is re-read and retried rather than trusted. Group-membership changes also propagate with a one-to-two-minute lag that made bare POSTs look successful while not persisting.
--no-owner-restore
Leaves each filter owned by the running admin account so the share survives. The trade-off is explicit: the filters end up owned by the migration account.
CarefulYou must be a member of every group a filter is already shared with, or the permission edit is rejected wholesale — not partially, wholesale. Check your own group memberships before a large run, and read the final report: it warns when shares did not persist.

10How do I prove any of this worked?

Open the workflow in the Cloud UI, run the transition, and compare filter row counts against the source. The run summary cannot tell you whether a rule does what it did.

The verification that counts

  1. 1Read `manual_review_<ts>.xlsx` end to end. It is a checklist of everything the tool could not do confidently. Working through it is not optional cleanup — it is the second half of the migration.
  2. 2Open a migrated workflow in the Cloud UI and look at the transition. A rule that exists and is misconfigured looks identical to a rule that works, in every API response.
  3. 3Actually run a transition that a translated validator guards, with input that should fail. A validator that no longer blocks anything is the default failure mode, and it is invisible until someone submits bad data.
  4. 4For automation, trigger the rule and read its audit log. A rule that imported successfully and runs as an actor with no permissions fails there and nowhere else.
  5. 5For filters, compare row counts against the source instance for a sample of ten. A filter that returns plausible results is the failure this whole tool exists to prevent.
Careful"41 of 43 workflows updated" is a proxy metric. It is compatible with 41 workflows whose validators no longer validate. Assume the rules are broken until a transition proves otherwise.

11rewrite_filter_refs — how do I run it?

The largest CLI in the repository, because JQL breaks in four independent ways and each fix is separately switchable. Plan, review the diff, then apply.

The run

cd rewrite_filter_refs && npm install && cp .env.example .env

# 1. Plan. Read-only. Writes the plan plus a reviewable diff of every filter.
node main/rewrite_filter_refs.js --plan-only --limit 25

# 2. Read what it intends to change.
node main/review_changes.js --plan-file logs/plan_<ts>.json

# 3. Apply, then verify.
node main/rewrite_filter_refs.js --resume
node main/verify_after_apply.js --plan-file logs/plan_<ts>.json

Scope and phase

FlagWhat it does
--plan-only / --execute-only / --resume / --plan-fileThe standard two-phase controls.
--limit <n> / --id-file <path> / --from-csv <path>Three ways to scope: a cap, an explicit filter-id list, or a CSV.
--include <pattern> / --name-prefix <str>Filter by name.
--only-changingPlan only filters the tool would actually modify.
--include-no-changeThe opposite — keep unchanged filters in the plan for a complete record.
--concurrency <n> / --batch-size / --dc-batch / --cloud-batchThroughput. Lower on 429.
--save-every <n> / --save-dry-runCheckpoint frequency, and persist the dry-run output for review.

The rewrites — each switchable, all on by default

FlagTurns off
--no-sanitizeThe JQL sanitizer (quoting, operator case, list handling).
--no-asset-rewrite / --no-asset-field-rewriteAsset references inside aqlFunction, and direct asset-field references outside it.
--no-order-by-cleanORDER BY cleanup for asset fields.
--no-priority-rewriteProactive rewriting of renamed priorities — a rename silently empties a filter, so leave this on.
--no-auto-cf-mapThe auto-built DC→Cloud custom-field map. Supply --cf-map <path> instead.
--no-quote-in-lists / --no-uppercase-opsCosmetic normalisations inside IN (…) lists and on operators.
--no-traffic-light-labelTraffic-light label handling in JQL values.
--rename-fields(opt-in) Rewrite field names that were renamed on Cloud.

What to do with things that cannot be rewritten

FlagWhat it does
--strip-broken-functionsRemove JQL functions that do not exist on Cloud, rather than leaving a filter that errors.
--strip-missing-projects / --skip-missing-projectsDrop unknown projects from the clause, or skip the filter entirely.
--validate-projectsCheck project names against Cloud before rewriting.
--strip-equality-missesRemove equality clauses whose value no longer exists.
--collision-resolve / --cloud-collision-map <path>How to resolve two Cloud objects sharing a name.
--asset-live-fallbackQuery Assets live when the cached map has no answer. Needs CLOUD_WORKSPACE_ID.

Ownership and sharing — read this before a large run

FlagWhat it does
--no-owner-swapDo not take ownership to edit. You can then only edit filters you already own.
--no-owner-restoreEdit as the admin account and leave it as owner. The trade-off is explicit: shares survive, but the filters end up owned by the migration account.
--skip-not-ownedSkip filters you do not own instead of swapping.
--no-share-org-admins / --org-admins-group <name>Control the org-admins share the tool adds.
--avoid-overwrite / --no-overwrite-checkGuard against clobbering a filter changed since the plan.
CarefulShare permissions are the trap. Adding a group to a filter's shares only sticks if the filter's owner can grant it — so swapping the owner to edit, then restoring it, silently drops the share, and the POST still returns 2xx. The tool verifies shares by re-reading and retrying, and the final report warns when they did not persist. --no-owner-restore avoids it entirely at the cost of ownership.
NoteYou must be a member of every group a filter is already shared with, or the permission edit is rejected wholesale — not partially. Check your own group memberships before a large run.

The companion scripts

review_changes.js
Print the before/after JQL for every filter in a plan. The review step.
verify_after_apply.js
Re-read the filters and confirm the JQL and shares actually landed.
rollback_plan.js
Restore the original JQL from the plan. Your undo.
refresh_plan.js, rebuild_from_live.js, rebuild_plan_from_original.js
Rebuild a stale plan against current state, from live filters or from the captured originals.
fetch_dc_originals.js
Pull the DC filter definitions the rewrite uses as ground truth.
enrich_dc_maps.js, enrich_typed_asset_map.js
Build and enrich the DC→Cloud id maps, including typed asset objects.
ensure_permissions.js, join_share_groups.js
Make the running account able to edit and share the filters in scope.
dump_cloud_name_collisions.js
List every place two Cloud objects share a name — read this before choosing a collision strategy.
repair_orphan_cf.js
Fix cf[N] references left pointing at fields that no longer exist.

12clone_workflow_rules — how do I run it?

Three modes on one CLI: fix workflows in place, clone them to another instance, or emit a ScriptRunner scaffold. Use --cloud-to-cloud whenever both ends are Cloud.

Cloud to cloud — the recommended path

cd clone_workflow_rules && npm install && cp .env.example .env

# Reads the NEW-format endpoint, so no old-to-new rule conversion is needed.
node main/clone_workflow_rules.js --cloud-to-cloud --collect
node main/clone_workflow_rules.js --cloud-to-cloud --validate-only
node main/clone_workflow_rules.js --cloud-to-cloud --apply

In-place fixes, and the ScriptRunner scaffold

# Normalise app Connect prefixes and remap custom fields on one instance.
node main/clone_workflow_rules.js --apply --update

# Emit extensions.yaml + Groovy stubs for a deployment tool.
node main/clone_workflow_rules.js --collect --export-scriptrunner-scaffold

Flags

FlagWhat it does
--collectRead workflows from the source and write them to --collect-dir.
--applyWrite to the target. Without --update it creates copies.
--updateUpdate the same-named workflow in place instead of creating a copy.
--validate-onlyRun Jira's own validation endpoint and stop. Always do this first.
--dry-runBuild everything, send nothing.
--cloud-to-cloud (--cc)Read via POST /rest/api/3/workflows, which already returns new-format rules. Skips the conversion table that is the biggest unknown in the tool.
--use-new-api / --use-legacy-apiForce the endpoint choice explicitly.
--project-keys <list> / --workflow-names <list> / --all-workflowsScope.
--name-suffix <str>Suffix for created copies. Default _v2.
--publishPublish the draft after applying.
--assign-schemesReassign workflow schemes to the new copies — the cutover step.
--source-url / --target-urlOverride the instances from .env.
--export-scriptrunner-scaffoldEmit extensions.yaml plus Groovy stubs.
--forceProceed past non-fatal guards.
TipCreating `_v2` copies and cutting over with `--assign-schemes` is deliberate. It makes the cutover a workflow-scheme reassignment — instant, visible in the UI, and trivially reversible — instead of an in-place edit that is none of those things.

fix_workflow_migrated.js is the narrow companion: it repairs workflows whose rules reference (migrated) field duplicates, in place, driven by TARGET_URL and CLOUD_API_TOKEN, with DRY=1 for validate-only.

13migrate_jsu_rules — how do I run it?

Reads exported OSWorkflow XML, translates app rules into native or Cloud-app equivalents, and updates the same-named Cloud workflow in place. Every run emits a review workbook.

The run

cd migrate_jsu_rules && npm install
cp .env.example .env && cp config.example.json config.json

# Export the workflows from the DC admin UI first, into ./xml/
node main/migrate_jsu_rules.js --collect --xml-dir ./xml
node main/migrate_jsu_rules.js --validate-only
node main/migrate_jsu_rules.js --dry-run
node main/migrate_jsu_rules.js --apply

Input and scope

FlagWhat it does
--xml-dir <path> / --workflow-file <path>Where the exported OSWorkflow XML lives, or a single file.
--collect / --collect-dir <path>Read and cache the Cloud catalogue the translation needs.
--no-fetchWork entirely from the cache. Useful offline, and much faster to iterate.
--project-keys <list> / --workflow <name> / --workflow-names <list> / --all-workflows / --allScope.
--exclude-projects <list>Skip projects.
--limit <n> / --concurrency <n>Cap and throughput.
--config <path>The run config. Field mapping file, id overrides, per-rule overrides, the app key.

Behaviour

FlagWhat it does
--validate-onlyValidate against Jira's endpoint and stop. Do this first, every time.
--dry-run / --applyBuild the payload without sending, or send it.
--confirmRequire an interactive confirmation before mutating.
--disable-jmweDo not emit Cloud-app (connect:*) rules at all — native equivalents only.
--runas-fallback <id>Account to use when a rule's run-as user cannot be resolved on Cloud.
--clean / --remove-stale-taggedRemove previously-applied tagged rules before re-applying. The way to re-run cleanly.
--fresh-startDiscard prior state and rebuild from the XML.
--allow-instance-mismatchProceed when the captured catalogue came from a different instance. Know why you are doing this.
--ignore-errorsContinue past per-rule failures instead of stopping.
--self-testRun the mapper's own test corpus. The fastest check that your config is sane.
--out <path>Where to write the report workbook.
CarefulTransitions are matched by name only. A DC common action that produced three same-named transitions gets the rule on all three — correct for a common action, wrong if two unrelated transitions happen to share a name. The workbook lists every fan-out; read it.
Note`manual_review_<timestamp>.xlsx` is produced on every `--apply`, `--validate-only` and `--dry-run`, with one tab per category needing human attention. It is a checklist, not a sign-off — a translated rule still deserves an expert eye in the Cloud UI.

Companion scripts

compare_xml_to_cloud.js
Diff the exported XML against what is actually on Cloud now — the honest answer to "did it apply".
fix_user_comparisons.js
Repair rules whose user comparisons reference DC usernames.
scripts/inventory_xml_baseline.js
Categorise every rule in the XML by plugin prefix, so you know what you are dealing with before translating anything.

14automation_rules_migrator — how do I run it?

Use the standalone scripts, in order — the interactive modes are unreliable. Four env vars drive everything, and the actor-permission step is not optional.

The pipeline that works

cd automation_rules_migrator && npm install

export AJ_SITE=your-site.atlassian.net
export AJ_CLOUD=<cloudId>
export AJ_USER=you@example.com
export AJ_TOKEN=<api token>

node export_all.js                       # 1. full export — this is your backup
OUT=mappings.json node gen_mappings.js   # 2. cache source→target id maps (slow, ~9 min)
node ensure_actor_access.js              # 3. give the rule actor the agent role
PLAN=1 node import_clean.js              # 4. dry preview
ACTOR_OVERRIDE=<accountId> node import_clean.js

The scripts

ScriptWhat it does
export_all.jsNon-interactive full export of every rule — summary plus per-rule body. Take this before anything else.
gen_mappings.jsGenerate source→target id mappings once and cache them, so audit and import do not repeat the slow lookup.
ensure_actor_access.jsAdd the rule actor to each target project's Service Desk Team (agent) role. Required or JSM rules fail to create.
ensure_addon_access.jsGrant the add-on project role (atlassian-addons-project-access) the permissions rules need. The fix for rules that import cleanly and then do nothing.
import_one.jsValidate the whole import path with a single rule, end to end. Do this before import_clean.js.
import_clean.jsImport the fully-mappable rules. Honours PLAN=1; auto-discovers and remaps the Assets workspace id.
reconcile_target.jsFix and enable rules that already exist on the target, in place — no import, no duplicates.
fix_migrated_refs.jsRepoint rules from off-screen (migrated) fields to their on-screen twins, in place.
repoint_actor.jsRepoint named rules' "Run rule as" to a chosen account.
enable_from_sources.jsEnable rules on the target to match source intent — the union of what was enabled in the sandbox and in Data Center.
audit_fields.js / audit_via_fix.jsReport which field references will not map to the target, since mapping is by field name.
CarefulBeing a site admin is not enough, and `/mypermissions` lies about it. Without real agent-role membership, JSM rule creation fails with 400 component.missing.permissions.actor while the permissions endpoint reports the permission as present. The automation engine checks role membership, not the grant. This costs an afternoon if you trust the API.
TipAlways set `ACTOR_OVERRIDE`. The source rule's actor is usually an app account with no permissions on the target. Rules created under it import successfully and then fail silently at run time — the worst of both outcomes.
NotePrefer `reconcile_target.js` when the rules are already there. After a migration assistant has carried rules across, importing creates a second copy of everything. Reconciling repairs what exists — and writes a full backup of every rule before touching one.

15Automation rules import, then do nothing.

The rule exists, is enabled, and its actor cannot act. Two different permission gaps produce this, and neither shows up as an import error.

The two gaps

SymptomCauseFix
400 component.missing.permissions.actor on createThe actor is not an agent on the target JSM project.ensure_actor_access.js. Site admin is not enough — the engine checks Service Desk Team role membership.
Import succeeds, rule never firesThe actor is a source-side app account with no permissions on the target.Re-import with ACTOR_OVERRIDE=<accountId>, or repoint the existing rules with repoint_actor.js.
Rule fires, actions failThe add-on project role lacks the permissions the actions need.ensure_addon_access.js.
Rule fires, writes the wrong fieldIt still references a (migrated) duplicate.fix_migrated_refs.js.
CarefulDo not trust `/mypermissions` here. It reports the permission as granted while rule creation still fails, because the two checks are not the same check. Read the rule's own audit log instead — it is the only place the real failure appears.

16Workflow validation fails and I cannot tell why.

Read the validation output rather than working around it. The tools surface Jira's own errors deliberately instead of auto-fixing them.

Common validation errors

Error shapeCauseFix
Unknown rule keyAn old-format rule type was converted to a key Cloud does not recognise.For cloud-to-cloud, use --cloud-to-cloud so no conversion happens. For DC, check the rule in the review workbook and map it by hand.
Missing custom fieldThe rule references a field that does not exist on Cloud.Create it, or let migrate_jsu_rules remove the broken rule — it reports every one it removes.
Missing status / transitionNames differ between instances.Fix the names, or supply an id override in config.json.
Opaque Connect config errorThe rule carries a stringified app config blob the tools deliberately do not parse.Recreate that rule by hand in the Cloud UI. There is no safe automated path.
Draft conflict on publishA draft already exists on that workflow.Discard the draft in the UI, then re-run with --publish.
Tip`--self-test` on `migrate_jsu_rules` is the fastest sanity check. It runs the mapper's own corpus with no instance involved, so a failure there means your config is wrong, not your tenant.

17I re-ran it and now there are duplicate rules.

That should not happen — every apply re-fingerprints the live target first. If it did, you are almost certainly in the wrong mode.

The three ways to get duplicates

Importing when you meant to reconcile
import_clean.js creates rules. If the rules are already on the target — because a migration assistant carried them — use reconcile_target.js instead, which fixes in place.
Applying without --update
clone_workflow_rules --apply creates _v2 copies by design. --apply --update edits the same-named workflow in place.
Re-applying after a rename
Fingerprinting matches on rule shape. If you renamed a workflow between runs, the tool cannot tell it is the same one. Use --clean --remove-stale-tagged to remove the previously-applied tagged rules first.
NoteThere is no rollback in this repository. Re-running with prior state is the only undo, which is why the export comes first — --collect for workflows, export_all.js for automation. Keep both off the machine running the migration.

18A rewritten filter returns the wrong results, or the shares vanished.

Two separate failures with two separate fixes. The share one is the quieter and the more damaging.

Symptom to fix

SymptomCauseFix
Filter returns fewer rows than the sourceA clause was stripped — an unknown project, a missing function, a value that no longer exists.Read the plan's per-filter diff with review_changes.js. Every strip is recorded with its reason.
Filter returns nothingA renamed priority or status still named in the JQL.Leave --no-priority-rewrite off; check the report for unresolved value rewrites.
Shares silently gone after applyThe owner was restored to someone who cannot grant that share.--no-owner-restore, accepting that the filters end up owned by the admin account. The final report warns when shares did not persist.
Permission edit rejected entirelyYou are not a member of a group the filter is already shared with.join_share_groups.js, or join the groups. Rejection is wholesale, not partial.
cf[N] still points at nothingThe field was deleted rather than renamed.repair_orphan_cf.js.
CarefulCompare row counts against the source for a sample of ten filters. A filter that returns plausible results is the exact failure this tool exists to prevent, and it is invisible unless you count.

19How do I prove any of this worked?

Run a transition. Trigger a rule and read its audit log. Count filter rows. The run summary cannot tell you whether a rule still does what it did.

The verification that counts

  1. 1Work through `manual_review_<ts>.xlsx` end to end. It is the list of everything the tool could not do confidently — the second half of the migration, not optional cleanup.
  2. 2Open a migrated workflow in the Cloud UI and look at the transition. A misconfigured rule looks identical to a working one in every API response.
  3. 3Actually run a transition that a translated validator guards, with input that should fail. A validator that no longer blocks anything is the default failure mode and is invisible until someone submits bad data.
  4. 4Trigger an automation rule and read its audit log. A rule running as an actor with no permissions fails there and nowhere else.
  5. 5Count rows for ten filters against the source instance.
Careful"41 of 43 workflows updated" is a proxy metric. It is entirely compatible with 41 workflows whose validators no longer validate. Assume the rules are broken until a transition proves otherwise.

What it does, and what it will not do

Stated up front, because discovering a limit mid-cutover is the expensive way to find it.

What it does

  • App workflow rules translated to native Cloud rules or the Cloud app equivalent, rule by rule
  • Every entity id remapped across instances — statuses, types, screens, events, roles, groups, priorities, resolutions, link types, security levels, custom fields
  • Workflows created as _v2 copies, so cutover is a scheme reassignment rather than an in-place edit
  • Automation rules exported in full before anything is imported or reconciled
  • Saved-filter JQL rewritten across four reference classes, including renamed priorities
  • Share permissions verified by re-read and retry, not trusted on a 2xx
  • A manual_review workbook naming everything the tool could not do confidently

What it will not do

  • It will not convert old-format rule types with certainty for DC to Cloud — use cloud-to-cloud mode where you can, and verify the rest in the UI
  • It will not parse or rewrite Connect and Forge rule config blobs, which are opaque stringified JSON
  • It will not translate field values — status names and option labels pass through verbatim; ids are mapped, values are not
  • It will not disambiguate transitions by from-status or to-status; matching is by name, so same-named transitions all receive the rule
  • It will not roll anything back. Re-running with prior state is the only undo, which is why the export comes first
  • It will not tell you a validator still validates. Only running the transition can
Apache-2.0 licensed. Free to use, fork and ship inside your own migration.

Moving workflows or automation between instances?

The toolkit is free and Apache-2.0. If you would rather not discover the actor-permission trap on cutover weekend, that is the kind of thing we do for a living.

View on GitHubAtlassian migrationsAsk on Discord