Find the app actor by accountType == 'app', never by the 557058: prefix. Measured across two sites, that prefix matched 7 of 121 and 6 of 101 app accounts, and it also matched real humans.
Send a scope ARI and a trigger event filter that disagree and the API returns 201, then silently rewrites the trigger to match the scope. Your source-vs-target diff will pass on a rule pointing at the wrong project.
There is a specific moment in an Atlassian migration when someone opens Automation on the new site and the room goes quiet. On a Data Center to Cloud move, the rules are all there and all grey — every one disabled, and the "Run rule as" column showing something nobody recognises. On a cloud-to-cloud move, the screen is empty.
Both of those are documented, expected behaviour. Neither is a bug. And in both cases the recovery is the same piece of work: set the actor on every rule, then enable it. This tutorial is that piece of work, done over the Rule Management API, with every claim measured against a live Jira Cloud site on 19 August 2026.
I am going to be blunt about which parts I ran and which parts I am quoting. The API behaviour below was measured on our own test tenant — I created rules, broke them on purpose, watched them fire, and deleted them. The two "what gets migrated" statements are quoted from Atlassian's own support pages, because I am not going to run a JCMA migration to prove a table.
First decide which road you are on
The two migration types fail differently here, and the difference is not a matter of degree.
Data Center → Cloud (JCMA)
Cloud → Cloud ("Copy product data")
Do rules come across?
Yes — project flows, global flows, or neither, your choice
No. Not at all
What state do they arrive in?
Disabled
n/a
Does the actor come across?
No
n/a
Prerequisite
Automation for Jira 7.2.6+, or A4J Lite 7.3.3+, enabled on DC
n/a
Your job afterwards
Set actors, re-point broken refs, enable
Export, remap, create, set actors, enable
6 rows × 3 columnsHeader row enabled
For Data Center to Cloud, Atlassian's migrate automation flows page is unambiguous on both counts. On state: "All migrated automation flows are disabled on the cloud by default post migration." And on what does not come with them: "The following features are not included in migration: Flow actors, Automation audit logs, Performance insights, Global configuration settings."
Read that second sentence again, because it is the whole article. Flow actors are not migrated. A rule with no valid actor cannot run, and as you will see in a moment, it cannot even be created over the API. JCMA hands you a complete set of rules that are structurally intact and functionally inert.
For cloud to cloud, the story is shorter and worse. The feature formerly called cloud-to-cloud migration now lives in the Atlassian organisation admin console as Copy product data, and its what data is copied page lists, under Jira project data:
and, under Jira Service Management project data, a second cross against Jira automation. There is no partial credit here and no configuration that turns it on. If you are consolidating two Cloud sites, your automation rules are not in the payload.
This is the same shape as the rest of a cloud-to-cloud move: the identity layer is shared at org level, the configuration layer is not, and the API is the only route in. I wrote about the equivalent trap for saved filters in Saved-filter JQL after a migration — the pattern repeats because the cause is the same. Anything that stores a numeric id or a tenant-scoped identifier gets re-bound to the destination, and nothing warns you.
The one route that works on both roads
Atlassian gives you a manual escape hatch that is worth knowing before we get to the API: Jira settings → System → Automation flows → More actions (…) → Export flows, which produces a single JSON containing all global and project-scoped flows, capped at 5 MB. Import is the mirror of it.
Its documented limits matter:
"All imported flows will initially be disabled and you'll have to enable them to use them." So even the manual route hands you a disabled set.
On a name collision, "the imported flow's name will become Copy of [flowname]".
It supports one-to-one migrations only — not consolidating several sites into one.
Coming from Server or Data Center, "data specific to your Jira instance such as Statuses, Issue types, or Fields and custom fields won't map correctly and those flows will need to be reconfigured."
That is fine for twelve rules. It is not fine for two hundred across forty projects, and it gives you nothing to audit afterwards. So: the API.
The API, and the path that will waste your first hour
The Rule Management API is reachable two ways, and both worked with a plain Basic-auth API token:
bash
1# via your site's gateway2https://<site>.atlassian.net/gateway/api/automation/public/jira/<cloudId>/rest/v1/...
3# or directly4https://api.atlassian.com/automation/public/jira/<cloudId>/rest/v1/...
The jira segment is a product slug. I pointed it at confluence on the same cloudId and got a clean 200 with an empty result set, so Confluence automation lives behind the same surface — useful if your migration has a Confluence half.
A plain Basic-auth API token worked on both hosts. Atlassian's API introduction says only that "Authentication requires an API token or browser session cookies", and the rule-management reference adds that Forge and OAuth 2.0 apps cannot access these resources — so treat this as a script's API, not an app's, and check that page yourself if you were planning to call it from inside a Forge app.
Now the hour-waster. The obvious listing path does not exist:
/rule is the create endpoint (POST) and the single-rule endpoint (/rule/{ruleUuid}). The list lives at /rule/summary. I lost real time to that 404 assuming my cloudId or my token was wrong, because a 404 on a gateway path looks exactly like an auth or tenancy problem. It is neither.
Pagination is cursor-based, and the links object is not what a naive loop expects:
next is a bare query string, not an absolute URL and not a path. Concatenate it onto your base path yourself. A loop that does if (next.startsWith(basePath)) — the shape that is correct for Confluence v1's _links.next — will fall through and stop after page one, and stopping after page one during a migration read is the kind of silent under-count that only shows up in the audit.
Here is a listing loop that handles it:
javascript
1constBASE=`https://${SITE}/gateway/api/automation/public/jira/${CLOUD_ID}/rest/v1`;23asyncfunctionlistAllRules(){4const out =[];5let query ="?limit=50";6while(query){7const res =awaitgetJson(`${BASE}/rule/summary${query}`);8 out.push(...res.data);9const next = res.links&& res.links.next;// "?cursor=...&limit=50" or null10 query = next ||null;11}12return out;13}
Each summary entry carries uuid, name, state, authorAccountId, actorAccountId, labels and ruleScopeARIs. To get the rule body — trigger, conditions, actions — you fetch GET /rule/{ruleUuid} per rule. There is no bulk body fetch, so budget one call per rule.
The create payload, and the 400 that names nothing
A create is a POST to /rule with the rule wrapped:
json
1{"rule":{ ... },"connections":[]}
The rule object accepts thirteen fields. I worked out which are mandatory the only way that is reliable: I took a rule that already existed on the site, round-tripped it successfully, then removed exactly one field at a time and recorded what came back.
Field
Omit it and you get
actor
400, generic
authorAccountId
400, generic
canOtherRuleTrigger
400, generic
name
400, generic
notifyOnError
400, generic
state
400, generic
trigger
400, generic
writeAccessType
400, generic
components
400 — "Automation rule must contain at least one valid condition or action."
ruleScopeARIs
400 — "The space scope for this rule is invalid"
collaborators
201, created fine
description
201, created fine
labels
201, created fine
14 rows × 2 columnsHeader row enabled
"Generic" means this, every time, for eight different fields:
json
1{"errors":[{"status":400,"code":"api.error.unknown",2"title":"The request body could not be parsed, please ensure the values provided are valid."}]}
So the practical rule for debugging a create is: that message means a required field is missing, not that a value is wrong. A field that is present but invalid gets you a precise message — send name: "" and you get "Enter a name for this rule."; send an actor that does not exist and you get told so. If you are staring at "could not be parsed", stop inspecting your values and start diffing your key set against the thirteen above.
Two more things I checked because our own internal runbook claimed otherwise:
state on create is honoured. I created with "state": "ENABLED" and read the rule back: ENABLED. Same for DISABLED. Our runbook said state was "not reliably honored on create" and recommended a follow-up enable call; on this site, on this date, that is simply not true. It matters in the safe direction and the dangerous one — if you replay a source export verbatim and the source rule was enabled, the target rule is live the moment it is created. If one of those rules sends email, it will send email. There is a whole article's worth of pain in Jira automation and notifications; I covered one corner of it in Suppressing the email when Jira Automation adds a comment.
Import everything disabled. Enable deliberately, in a second pass, after you have looked at the list.
Duplicate names are legal. Two rules with byte-identical names both returned 201. Hold that thought — it becomes important shortly.
The actor is the whole job
This is where a rule migration actually succeeds or fails, so it gets the most space.
There are exactly two ways an actor fails, they have different messages, and they need different fixes. Both reproduced identically on a second run:
text
1actor is an accountId that does not exist on this site
2 400 "The selected actor does not exist. Please chose an actor that exists.
3 If the problem persists, please reload and try again."
45actor exists here, but is deactivated or has no product access
6 400 "The selected actor does not have access to this product. Please choose another user."
(The typo in the first one is Atlassian's, not mine. Match on a substring, not the whole string.)
The second failure is the one that ambushes people, because it is not about migration at all. On the site I measured, 11 of the 18 existing rules ran as a deactivated user account, and 2 more ran as the Automation for Jira app. That is not unusual or careless — it is what happens over a few years when the admin who built the automation leaves. Those rules keep working, because Automation does not re-validate the actor of a rule that is already running. The moment you try to create the same rule on a new site, the actor is validated — and a deactivated one is rejected with the second message above, every time.
So before you write a single line of import code, run this over your source export:
javascript
1// group the source rules by actor, then resolve each actor on the TARGET2const actors =newSet(rules.map(r=> r.actorAccountId).filter(Boolean));3for(const accountId of actors){4const u =awaitgetJson(`${TARGET}/rest/api/3/user?accountId=${encodeURIComponent(accountId)}`);5console.log(accountId, u.accountType, u.active, u.displayName);6}
Anything that comes back missing, active: false, or absent from the target is a rule you must re-point before you import it, not after.
How to find the app actor — and how not to
The usual answer to "who should the rule run as" is: the same actor native Jira rules use, which is the Automation for Jira app account. That gives the rule app-level permissions instead of borrowing some person's.
Our internal runbook told us to find it by looking for account ids beginning with 557058:, on the reasoning that app accounts share that prefix. I checked that, because the whole point of a runbook entry is that someone will follow it at 1am. It is wrong, and it is wrong in both directions.
On the site I was working on, the user-search endpoint returned 146 accounts — 121 apps, 21 people and 4 portal customers. Splitting the apps and the people by id prefix:
accountId prefix
app accounts
human accounts
557058:
7
2
712020:
93
13
legacy 24-hex, no colon
21
5
70121:
0
1
5 rows × 3 columnsHeader row enabled
So the prefix matched 7 of 121 app accounts — it misses 94% of them — and it simultaneously matched two real people. A "most common 557058: account" heuristic would have been choosing from a pool of nine, two of whom were humans.
I checked a second, unrelated Cloud site to make sure this was not one tenant's history: 101 app accounts there, 6 with the 557058: prefix, 75 with 712020:, 20 legacy — and three humans carrying 557058:. Same conclusion, replicated.
The correct test is one field, and Jira will just tell you:
javascript
1asyncfunctionfindAutomationAppActor(site){2// /users/search is offset-paginated — one call is NOT the whole directory3const users =[];4for(let startAt =0;; startAt +=200){5const page =awaitgetJson(6`${site}/rest/api/3/users/search?startAt=${startAt}&maxResults=200`);7 users.push(...page);8if(page.length<200)break;9}10const app = users.find(u=> u.accountType==="app"11&& u.displayName==="Automation for Jira");12if(!app)thrownewError("Automation for Jira app account not found on target");13return app.accountId;// use as { type: "ACCOUNT_ID", actor: <this> }14}
accountType is app, atlassian, or customer. It is authoritative, it is in the standard user API, and it does not depend on when the account was minted.
One genuine surprise while checking this: the Automation for Jira app account id was identical on both sites — the same 557058: id, character for character, on two tenants in two different organisations. It is a global Atlassian product identity, not a per-site one. So the common advice to "rewrite the app actor for the target" is not automatically necessary for A4J. Do not blindly rewrite it, and do not blindly copy it either — resolve it on the target and confirm it exists. That is one API call, and it converts an assumption into a fact.
If you want actions attributed to a person instead, that is a deliberate choice with consequences: the rule inherits that person's permissions, and on a JSM project that user must be an agent in the Service Desk Team role or the rule will fail at run time rather than at create time. Grant the role first.
The scope ARI, and the rewrite you will never see
Every rule carries ruleScopeARIs, and the ARI carries the cloudId. Two shapes exist:
text
1ari:cloud:jira::site/<cloudId> # a global rule (note the empty segment)
2ari:cloud:jira:<cloudId>:project/<projectId> # a project-scoped rule
The cloudId sits in a different position in each, which is exactly the sort of detail a regex gets almost right. Both must be regenerated for the target tenant, and the project id must be remapped too — a source project id is meaningless on the destination, or worse, meaningful and wrong.
Leave a source cloudId in place and you get this:
text
1400 "User does not have admin permission within this rule home"
That message is actively misleading. It reads like a permissions problem, and you will go and check your permissions, and your permissions will be fine. The cause is a cloudId the target does not own — there is no "rule home" there to be an admin of. If you see that error during an import, look at your ARIs before you look at your grants.
Now the part I would not have believed without running it.
A rule's trigger has its own copy of the project reference, in trigger.value.eventFilters, separate from ruleScopeARIs. So a remapper can easily fix one and forget the other. I built exactly that mistake on purpose: scope pointing at project A, trigger event filter still pointing at project B, everything else valid.
The API silently rewrote the trigger to match the scope. It did not error, it did not warn, and it did not report the change.
To be sure that was real behaviour and not a display artefact, I enabled the rule and fired it. The rule's action was a status transition. I created one issue in each project and commented on both:
The issue in the scope project moved To Do → In Progress within 5 seconds.
The issue in the project named by the trigger filter I sent was still in its original status when I stopped watching at 25 seconds.
A control rule whose scope and trigger agreed transitioned its own issue in under 4 seconds, so the rig itself was sound.
The good news is that you cannot accidentally build a rule that watches a project outside its own scope. The bad news is the shape of the failure this creates:
ruleScopeARIs is the single source of truth, and if you get it wrong, the API will forge a matching trigger for you. Remap the scope to the wrong project and the rule will run happily against the wrong project, while a source-versus-target diff of the rule JSON shows the trigger matching perfectly — because the server wrote it to match. Your audit passes. The rule is wrong.
This is the same class of failure as a post-migration field audit that counts to zero and reports success, which I took apart in A wrong field ID counts to zero. The lesson generalises: an audit that only compares the destination to itself will confirm anything the destination did to your data. Assert the scope ARI against your mapping table, not against the rule you just created.
Names: three tools, three different behaviours
The site I measured had 18 rules and 4 distinct names. Two names appeared eight times each — the same rule cloned across eight projects, which is completely normal in a mature Jira.
Now watch what the three available routes do with that:
Route
Result for 8 identically-named rules
Rule Management API
All 8 created. Duplicate names are accepted (measured: 201)
UI import of the export JSON
Renamed on collision — "the imported flow's name will become Copy of [flowname]"
A script that dedupes by name
1 lands. 7 vanish, silently
4 rows × 2 columnsHeader row enabled
That third row is not hypothetical — dedupe-by-name is a sensible-looking idempotency guard, and it is in plenty of import scripts, including one of ours. On this site it would have landed 4 rules and skipped 14, with a log line saying "skipped, already exists" for each. Every skip looks like success.
There is a fossil of the second row on the site too: one of the 18 rules is literally called "Copy of Test-1231", which is the UI import telling you it hit a name collision, months ago, to nobody in particular.
If your idempotency key is the rule name, change it. Key on the source rule uuid instead, and keep your own mapping of source uuid → target uuid. It is the only identifier that is stable and unique, and you need that mapping for the audit anyway.
Enabling and deleting: two small cliffs
Two lifecycle details cost me time, and both are cheap to get right once you know.
The state endpoint's body field is value, not state.
bash
1PUT /rest/v1/rule/{ruleUuid}/state
2{"value":"ENABLED"}# 2003{"state":"ENABLED"}# 400, "could not be parsed" — no clue which field
The obvious guess is wrong and the error does not help. Worse, if you fire that PUT without checking the response code, your script "disables" a rule that stays enabled. That is how I ended up with live probe rules on a test site — my own helper only set a Content-Type when there was a body, so my cleanup calls failed and I did not look.
DELETE needs a Content-Type header even though it has no body.
bash
1DELETE /rest/v1/rule/{ruleUuid} ->415, empty response body
2DELETE /rest/v1/rule/{ruleUuid}-H'Content-Type: application/json' ->200
A 415 with an empty body, on a request with no body, is not a helpful signal. Set the header on every call and forget about it.
And a rule can only be deleted once it is disabled:
text
1DELETE on an enabled rule -> 400 "Rule cannot be deleted unless it is already disabled."
So rollback is always two calls: disable, then delete. Build that into your teardown from the start, because you will be tearing down a lot during rehearsals.
The order that works
1
Resolve the target's actors first
before writing any import code, take the distinct actorAccountId values from your source export and look each one up on the target with GET /rest/api/3/user?accountId=…. Record accountType and active. This is the step that tells you how much work the migration actually is.
2
Find the Automation for Jira app account on the target
accountType === "app" and displayName "Automation for Jira". Never match on the 557058: prefix. Fail loudly if it is missing rather than falling back to a human.
3
Build the mapping table
source project id → target project id, and source cloudId → target cloudId. Regenerate every ARI from the mapping; do not string-replace the cloudId and hope, because the ARI shape differs between global and project scope.
4
Rewrite each rule
set actor to the resolved target actor, set ruleScopeARIs from the mapping, and force state to DISABLED regardless of the source state. Keep only the thirteen create fields; drop uuid, created and updated.
5
Create, keyed on source uuid
POST to /rule, store the returned ruleUuid against the source uuid in your mapping file. Never dedupe on rule name.
6
Audit against the mapping, not against the target
for each created rule, assert that its ruleScopeARIs equals what your mapping table says it should be. Do not compare the trigger's event filters to the source; the server rewrote them, so that comparison is guaranteed to pass and proves nothing.
7
Enable in a reviewed second pass
PUT /rule/{uuid}/state with {"value":"ENABLED"}, checking the response code every time. Leave anything that sends email or notifies customers for last, and turn those on with someone watching.
What I did not test, and where I would be careful
Being straight about the edges, because a migration plan built on my confidence is worth less than one built on my evidence.
I did not run a JCMA migration. The two Data Center to Cloud statements — flows arrive disabled, flow actors are not included — are quoted from Atlassian's support page as it read on 19 August 2026, not observed. Everything about the Rule Management API's behaviour is observed, and that is the part you will actually be writing code against on both roads.
Jira Data Center has no public automation REST API. There is an internal /rest/cb-automation/latest/... surface that community posts describe and that some tools lean on, but it is undocumented and unsupported. If you need rule bodies out of Data Center, use the UI export. Building a migration on an internal endpoint is a decision to be surprised later.
Component types are not documented, and guessing one costs you a 500. I tried to build a comment action by hand from a plausible type string and got 500 with an empty response body on every variation — not a 400, not a validation message. Do not hand-author component JSON. Take a real rule from the source export, keep its component blocks verbatim, and change only the identifiers your mapping table covers.
The JSM agent-role requirement is experience, not a measurement here. If you point a rule's actor at a person on a Jira Service Management project, that person needs to be an agent in the Service Desk Team role for the rule's actions to succeed. That comes from our own engagement notes and Atlassian's permission model, not from anything I ran for this article — I measured actor validation at create time, not action failures at run time.
One measurement, two tenants, one date. Error strings and validation behaviour are the softest thing in this article. Everything here was measured on 19 August 2026, against Jira Cloud on the Free plan — which is itself worth knowing, since it means the Rule Management API is not gated behind a paid edition. Match on substrings, log the whole error body, and re-check before a cutover rather than trusting a table in a blog post, including this one.
The measured facts I would stake the plan on: the actor must be resolved on the target and never copied blindly, ruleScopeARIs is the only scope that matters because the server will rewrite the trigger to agree with it, and your idempotency key is the source uuid. Get those three right and the rest is a loop with good error logging.
If you are earlier than this — still working out what a move will cost and what will break — that is a different exercise, and Making Atlassian Cloud Migrations Predictable is where I would start instead.
One person, two JSM customer accounts: what actually clears the duplicate after an email change