Your migrated workflow rules still run. That is not the same as still working
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
A JSU condition of priority > 5 persisted on Cloud as priority = 5, because the target rule only accepted = and !=. The comparator was silently demoted.
Data Center’s REST API does not return workflow rule bodies:
GET /rest/api/2/workflowgives summaries (name, description, steps)./rest/workflowDesigner/1.0/workflows?name=Xreturns the workflow’s layout and its rule counts.
You can count the rules without being able to read them.
Run order is not cosmetic:
- Filters go first.
- Workflows next, so the transitions exist.
- App‑specific rules inside those workflows (the step this article focuses on).
- Automation last, because an automation rule references fields, statuses and workflows that must already exist for its own id mapping to resolve.
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.
The silent validator defect
When issues fail to arrive, you find out. A project can be short four hundred issues, a custom field 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.
Comparator demotion case study
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.
Our tooling fix
Our tooling now translates 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.
An honest caveat 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.
Reading rules out of Data Center
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/workflowreturns 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 failure modes
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 naïvely, 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). 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.
Quantifying the defect
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.
-
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 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.
Migration order matters
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.
-
App‑specific rules inside those workflows (the step this article is mostly about).
-
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.
Verification principles
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
GET /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
Comments
No comments yet. Start the discussion.