Notes from the work

One email when a tutorial or migration write-up goes live. Nothing else, and an unsubscribe link in every email.

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

Company

  • Blog
  • Tutorials
  • Contact

Community

  • Join Discord
  • Support this site

© 2026 LeanZero. All rights reserved.

Privacy PolicyTerms of Service24/7 SupportTrust CenterSecurityDPALegal notice
  1. Home
  2. Blog
  3. Migrated Workflow Logic Does Not Look Broken
Your migrated workflow rules still run. That is not the same as still working
Cloud MigrationJira

Your migrated workflow rules still run. That is not the same as still working

Gabriela Perdum
Gabriela Perdum
Author
11 min readSeptember 7, 2026

Key takeaways

  • Missing data leaves a hole you can see. Broken logic leaves a rule that is present, enabled and wrong — there is nothing to notice.
  • Measured: a JSU condition of priority > 5 persisted on Cloud as priority = 5, because the target rule only accepted = and !=. The comparator was silently demoted.
  • DC's REST API does not return workflow rule bodies. /rest/api/2/workflow gives summaries; workflowDesigner gives rule COUNTS. You can count the rules without being able to read them.
  • Run order is not cosmetic: filters, then workflows, then app rules inside those workflows, then automation last — automation depends on the fields and workflows already existing.
  • Verify by behaviour, not by presence. A rule that appears in the Cloud UI has told you nothing about whether it still does its job.

There is a particular kind of migration defect that nobody catches during cutover weekend, and it is not the kind everyone prepares for.

When issues fail to arrive, you find out. A project is short four hundred issues, a custom field is blank across the board, an attachment link 404s. There is a hole, someone falls into it, and it gets raised. Unpleasant, but self-reporting.

The logic around those issues is different. A workflow validator that no longer validates does not throw. It does not appear in a report. It sits in the transition exactly where you left it, enabled, correctly named, and it waves through the thing it was written to stop. You find out weeks later, when someone asks why a ticket reached Done without an approval, and the answer is that it has been doing that since the migration.

The one that convinced me to write this down

Here is a real case, and it is the most benign-looking bug I have seen in a migration.

A workflow-extensions condition on Data Center checked priority > 5. Ordinary stuff — block the transition unless the priority is above a threshold. It migrated. It arrived on Cloud as a native rule, present and enabled in the transition, with the same field and the same value.

It arrived as priority = 5.

The Cloud rule it was translated into only emitted two comparators, = and !=. Anything else collapsed into the nearest one it could express. So a greater-than became an equals, and the condition went from "priority is above five" to "priority is exactly five" — which is false almost always, in a rule whose whole job was to be true most of the time.

Nothing errored. The migration report counted a workflow moved. The rule appears in the Cloud UI with a sensible description. An admin reviewing the transition sees a condition on priority and moves on, because it looks like the condition that was always there.

That is the shape of this entire category. The rule is present. The rule is wrong. There is no signal.

The fix in our own tooling was to translate the full set — six comparators (>, >=, =, <=, <, !=) across five comparison types (STRING, NUMBER, DATE, DATE_WITHOUT_TIME, OPTIONID), taken from the source app's own constants rather than guessed. And an honest caveat that survived the fix: for STRING and OPTIONID, Cloud genuinely does not support ordered comparison, so > and < still demote to != and =. That case cannot be repaired, only reported, which is the difference between a tool you can trust and one you cannot.

You can count the rules. You cannot read them

Before you can check any of this, you have to get the rules out of Data Center, and this is where the ground gives way.

GET /rest/api/2/workflow returns summaries. Name, description, steps. Not the transition rule bodies — not the conditions, validators and post-functions that are the entire subject of this article. Those are not exposed by any documented REST endpoint.

There is an endpoint that gets tantalisingly close. /rest/workflowDesigner/1.0/workflows?name=X returns the workflow's layout and its rule counts. So you can learn that a transition has three validators. You cannot learn what any of them does.

The obvious fallback is the admin UI, which does show them. That is gated behind WebSudo — the re-enter-your-password prompt — and WebSudo cannot be passed programmatically when 2FA is enforced on the account, which on a well-run instance it is.

So the supported route is neither: export the workflows as OSWorkflow XML from the DC admin UI, and read the XML. That is not a workaround for a gap in the API. Given the API, the admin pages and the auth model, it is the only door that opens.

I want to dwell on the counts endpoint for a second, because it is the perfect miniature of the whole problem. It will happily tell you that the number of rules matches on both sides. Three before, three after. A tidy number, a green tick, and no information whatsoever about whether any of the three still works.

Three ways a rule survives the move and stops working

The comparator demotion above is one flavour. There are at least two more, and they fail differently enough that a single check will not catch all three.

The macro that becomes literal text. Workflow-extension apps use runtime macros — %%CURRENT_USER%% and friends — that get substituted at evaluation time. Cloud has no equivalent. Passed through naively, the rule persists with the macro as a string: a condition comparing a custom field against the literal characters %%CURRENT_USER%%. It is a valid rule. It saves without complaint. It is false forever, because no field ever contains that text. The right behaviour is to refuse the translation and tell the operator, which is what we ended up doing — a dedicated sheet in the review workbook listing every macro that has no Cloud equivalent, because a rule you were warned about is recoverable and a rule that silently compares against a string is not.

The expression that is always false. Cloud rules can carry Jira Expressions, and an expression referencing a property that does not exist does not error — it evaluates falsy. A condition checking group or role membership via user.roles looks entirely reasonable, and there is no such property on the Cloud user type. The expression is syntactically fine, the rule saves, and it denies everything. Getting this right meant reading Atlassian's own User type reference and rewriting to user.getProjectRoles(issue.project), and the reason it is worth naming here is that the failure mode of a wrong property is silence, not a stack trace.

There is a related trap in the same family: Jira Expressions does not permit dot-access to custom field ids, so issue.customfield_10100 is not valid where issue["customfield_10100"] is. I have not established whether that one errors or simply evaluates falsy like the others, so treat it as unknown rather than assuming it will announce itself.

The rule that is dropped and not counted as dropped. Some rules get rejected on apply as unrecognised. If your tooling classifies those as "not a real rule" rather than "a real rule I could not translate", they vanish from both the target and the report. We hit this with previous-status validators arriving from a prior migration — legitimate rules, silently discarded on every run, because the allowlist did not know their short name.

How many of these are there, really

The honest answer is that nobody can tell you a percentage, because the population depends entirely on which apps a given instance used and how heavily. What I can give you is one instance's number.

Comparing the exported DC workflow XML against what actually landed on Cloud, our own audit tool bucketed every discrepancy by root cause: field unresolved, catalogue miss, field unmapped, status unmapped, no mapper, and a residual MISSING_OTHER bucket for rules DC had and Cloud did not. On one migration that residual bucket alone held 90 entries — ninety rules present on Data Center and absent on Cloud, none of which had produced an error anywhere.

Two things to take from that number rather than the number itself.

The first is that it took a comparison to find them. Not a report, not a count, not an inspection of the Cloud side. You cannot find an absent rule by looking at what is present; you can only find it by holding the two sides against each other. That means keeping the DC export after cutover, which is the step people skip because the migration is "done".

The second is the shape of the bucketing. "Manual review: 90" is not actionable and gets deferred forever. Ninety split into macro / status / field / no-mapper, each in its own tab, is a morning's work with an obvious starting point. When we lumped status-reference failures in with generic field failures, the operator could not tell which rules failed for what reason, and the whole list went untouched. Categorising the failure is most of the fix.

The order is not cosmetic

Four things need moving, and they depend on each other in one direction only.

Filters go first. Everything downstream is verified using JQL, and verifying with JQL that itself references dead custom field ids or a DC filter id is how you conclude that a working thing is broken or, much worse, that a broken thing is fine. Saved filters copy body-for-body, so their JQL still names DC ids, DC asset fields, and functions Cloud does not have. The filter loads and quietly returns the wrong set.

Workflows next, so the transitions exist. Then the app-specific rules inside those workflows, which is the step this article is mostly about. Then automation last, because an automation rule references fields, statuses and workflows that must already exist for its own id mapping to resolve.

Automation carries its own distinct problem, which is the rule actor rather than the rule body — the account a rule runs as usually has no permissions on the target, and being site-admin is not sufficient. That has its own article, and it is worth reading before you touch automation, because it is the one failure in this set that does surface an error.

What "verified" has to mean here

If the defect has no symptom, then "I checked and it looked fine" is not a check. A few principles came out of building this that generalise past our own tooling.

Validate before mutate, against the platform's own validator. Jira exposes /rest/api/3/workflows/update/validation. Every write should go through it first and surface what it says rather than auto-correcting, because an auto-correction is exactly how a > becomes an = without anyone deciding that.

Re-fetch the live target before every apply. Fingerprint what is actually there, now, rather than trusting a plan built ten minutes ago. A stale plan that double-applies is its own silent corruption, and the mistake is easy: the plan is internally consistent, so it looks right.

Be additive, and be explicit about the one exception. Tooling should add and correct, not delete rules it did not recognise. Where deletion is genuinely correct — a rule referencing a custom field that does not exist on Cloud, and therefore already broken — it must be reported individually, not summarised as a count.

A plan is valid for exactly one source-target pair. Re-running an apply against a different Cloud target does not fail; it pushes custom field ids from the first tenant into the second. This is the same class of error as a migration plan stamped for one tenant pair and replayed against another, and it deserves the same fingerprint gate.

Produce a workbook, not a log. A run should emit a reviewable file with one tab per category needing a human — unmappable macros, unresolved statuses, unresolved fields, no-mapper — because "manual review" as a single bucket tells an operator nothing about where to start. And that workbook is a checklist, not a sign-off. A translated rule still deserves an expert eye in the Cloud UI.

Cloud to Cloud is not exempt

It is tempting to read all of this as a Data Center problem that goes away once you are on Cloud. It does not. Every entity id is scoped to its site — statuses, issue types, screens, events, roles, groups, priorities, resolutions, link types, security levels, custom fields. A workflow copied verbatim from one Cloud site to another is a workflow full of references to ids that mean something else, or nothing, on the target.

The difference is that Cloud-to-Cloud at least gives you REST on both ends, so the rules are readable without the XML detour. The failure modes underneath are the same: a rule that resolves to the wrong entity is still a rule that is present, enabled and wrong.

The other Cloud-to-Cloud specific is app rules carrying a Connect prefix that differs between the two sites, which leaves rules that look correct in the JSON and match nothing at runtime. Normalising those prefixes is its own step, and it is invisible unless you go looking.

What to go and check on Monday

If you have migrated in the last year and none of this was on your list, three checks will tell you most of what you need to know.

Take your five most business-critical transitions and open the conditions and validators in the Cloud UI. Not the count — the actual configuration. Compare each against what the DC workflow XML says it was. You are looking for comparators that changed, values that are now string literals, and rules that are simply absent.

Then take any saved filter that a dashboard or an automation depends on and check its JQL for cf[ references, customfield_ ids and filter ids. If it runs and returns results, that is not evidence of anything; the question is whether the results are the ones it used to return.

Then pick one workflow rule you know should block something, and try to do the thing it blocks. Behaviour is the only verification that survives this category of defect. Presence is not.

The four tools I have been describing are open source and free — the Jira Workflow & Automation Toolkit, Apache-2.0, public REST only. They came out of doing this work on real migrations, and they exist because the alternative was checking several hundred transitions by hand and still not being sure.

But the tools are the smaller half. The larger half is the habit: after a migration, a rule that is present has told you nothing. Go and make it refuse something.

ShareXLinkedIn
Previouslsof: command not found in a packaged Mac app, and the port reclaim that never ranNextI said the certificate was a receipt. Then I collected four of them in two months

Tags

#atlassian-migration#dc-to-cloud#jira#workflow#automation#jql

Comments

0

Add a comment

Confirm your address using the email we send. You can unsubscribe at any time. Your email stays private and this does not sign you up for the newsletter.

Enjoyed?

Join our Discord community to discuss this article and get help with your own migration projects.

Discuss in Discord

On this page

  • The one that convinced me to write this down
  • You can count the rules. You cannot read them
  • Three ways a rule survives the move and stops working
  • How many of these are there, really
  • The order is not cosmetic
  • What "verified" has to mean here
  • Cloud to Cloud is not exempt
  • What to go and check on Monday

Join the Community

Join the community to ask questions, share your work, and connect with other professionals.

Join Discord

Found My Advice Helpful?

If I helped you solve something for free, coffee money is always appreciated (but never expected).

Buy Me a Coffee

Related reading

Customer Service Management pricing: Atlassian's $1 AI agent resolution, and when it pays

From 3 December 2026 the AI agent in Atlassian Customer Service Management bills $1.00 per resolution, from the first one, with no allowance. Jira Service Management's own self service agent bills Rovo credits instead: on our Service Collection Premium site it used exactly 10 credits per reply. Which meter you're on, what Atlassian counts as a resolution, and the break-even against agent time.

10/21/2026

Jira CAB approval: a change advisory board process in Jira Service Management, end to end

We built a CAB on our Jira Service Management test site on 17 October 2026: an approver group, a rule that routes high-risk changes to it, the change calendar and a freeze window. The approval step was already in the template. A low-risk change with no approver group got stuck, three transitions out of Review can skip the approval, and freeze blocking was off until we ticked a box.

10/20/2026

“This app might stop working soon”: what admins do before Connect end of support

Connect end of support begins on 31 January 2027. On our test site the Connected apps page tagged 4 of 57 entries Legacy, said January 2027 in its banner and December 2026 in its tooltip, and had no column or tag saying Connect or Forge. Here's how to find your affected apps and what to ask each vendor.

10/17/2026