RFC-147: App Custom Migration Progress

RFCs are a way for Atlassian to share what we’re working on with our valued developer community.

It’s a document for building shared understanding of a topic. It expresses a technical solution, but can also communicate how it should be built or even document standards. The most important aspect of an RFC is that a written specification facilitates feedback and drives consensus. It is not a tool for approving or committing to ideas, but more so a collaborative practice to shape an idea and to find serious flaws early.

Please respect our community guidelines: keep it welcoming and safe by commenting on the idea not the people (especially the author); keep it tidy by keeping on topic; empower the community by keeping comments constructive. Thanks!

Project Summary

Atlassian is exploring an improvement to Forge app migration progress reporting that would give apps the option of reporting progress more accurately without facing timeouts. The proposal would let apps declare custom migration event types, including estimated totals and weights, then update completed counts during long-running or multi-phase imports. This would help avoid legitimate migrations being treated as stalled, improve customer visibility into progress, and reduce partner workarounds, while preserving the current behaviour for apps that do not need the new APIs.

  • Publish: 17 September 2026
  • Discuss: Until 01 October 2026
  • Resolve: By 15 October 2026

Problem

With Data Center to Connect migrations, Atlassian required App Partners’ cloud apps to send updates that included the percentage completion of a given migration. When completion reaches 100%, the migration is complete. This model worked well for some apps, while others found it difficult to calculate progress, and the inconsistency of progress calculation across the app migration landscape resulted in customer confusion.

The Forge migration model calculates progress from migration events and acknowledgements, not from a partner-reported percentage:

  • The server app uploads app data, creating events such as avi:ecosystem.migration:uploaded:app_data.
  • The Forge app processes each event and calls messageProcessed(transferId, messageId) when the import succeeds, or messageFailed(transferId, messageId) if it fails.
  • Customer-facing progress advances as uploaded app data events are acknowledged as processed.
  • The documented 15-minute inactivity timeout is reset when new app data is uploaded or an app data message is acknowledged.
  • If neither happens within that window, the transfer is treated as stalled and fails.

In short, Forge progress reflects observed migration activity rather than an arbitrary completion percentage reported by the app.

However, we’ve observed and heard that the Forge model also does not work well for some apps, leading to workarounds that have required significant effort from app partners. These include, but are not limited to:

  • Sending a large number of dummy app data events that are used just in case part of the import takes longer.
  • Carefully ordering and scheduling event processing so fast parts of an import are interspersed with slower parts that take more than 15 minutes.

We’ve heard this is a particular problem for apps that need to do post-processing using Jira or Confluence APIs, or that have a multi-phase import process.

Returning to the Connect solution is not a realistic option. Some reasons include:

  • Apps have already implemented the Forge solution, and this could break backwards compatibility. We are ideally seeking a solution that adds to what we have now.
  • Atlassian systems are now an opinionated contributor to Forge app migration progress calculations. Calculation of progress needs to factor in both Atlassian and app progress.
  • For some apps, the Connect solution resulted in a confusing customer experience. For many simpler apps, the current approach requires no special thought and works well for them.

Any solution must not compromise migration reliability; Atlassian has seen examples of customers dropping apps when moving to Cloud because migrating is deemed to not be worth it. To mitigate this, we need a solution that:

  • Avoids causing migrations to fail when they are genuinely still in progress.
  • Does not slow down migrations significantly.
  • Provides customers with good observability into how their app migration is going.
  • Is easy for app partners to implement properly.

Proposed Solution

Overview

Our current approach calculates progress as (effectively):

image

If we choose to address this, we’d instead implement something like:

image

There would be n different types of events, with i=0 representing the existing Atlassian-tracked events (with weight 1), and zero or more custom event types with weights, total count, and completion counts optionally supplied by the app (defaulting to there being no custom event types). If apps do nothing, the current behaviour would be retained (i.e. this would be an optional improvement for apps that would benefit from it).

Declaring custom event types

At the start of the import, apps would have the option of making an API call with a description for each type of event (which in the future we could show to the customer to provide more detail about progress), and the weight and initial estimate of the eventsTotalCount for that event type. The weights and descriptions would be locked in for the migration.

POST /app/migration/forge/v1/customeventtypes/declare HTTP/1.1
Authorization: Bearer yourToken
Content-Type: application/json

{
  "transferId": "d37c7caa-ff0d-4e46-b815-2edc4dc3384c",
  "eventTypes": {
     "my-custom-event-type-1": {
       "description": "Type 1s processed",
       "totalCount": 50,
       "weight": 3.0
     },
     "my-post-processing": {
       "description": "Jira issues updated",
       "totalCount": 3000,
       "weight": 0.1
     }
  }
}

HTTP/1.1 201 Created
Content-Type: application/json

{"status": "success"}
  • Apps may choose their preferred set of event types for a transfer.
  • Event types can only be declared once per transfer.
  • Any event type that is not declared cannot be updated later.
  • The declare operation is idempotent, but it cannot be called again with different values for the same transfer.

Updating custom event type statuses

Apps that have made such a call would be free, from time to time, to make another API call (rate-limited to 5 per transfer per 5-minute period):

  • Increasing the eventsCompleted values for the custom events. These would be locked to only increasing. Increasing an eventsCompleted value would reset the migration timeouts. If there is no increase, it would be processed idempotently, but would not reset the timeout again.
  • Updating the eventsTotalCount to a newer estimate. This could increase or decrease, but not below the eventsCompleted value for that event type.
POST /app/migration/forge/v1/customeventtypes/update HTTP/1.1
Authorization: Bearer yourToken
Content-Type: application/json

{
  "transferId": "d37c7caa-ff0d-4e46-b815-2edc4dc3384c",
  "eventTypes": {
     "my-custom-event-type-1": {
       "completed": 10
     },
     "my-post-processing": {
       "totalCount": 2000
     },
     "invalid": {
       "totalCount": 1
     }
  }
}

HTTP/1.1 200 OK
Content-Type: application/json

{
  "my-custom-event-type-1": {
    "status": "success"
  },
  "my-post-processing": {
    "status": "error",
    "message": "New totalCount less than current completedCount"
  },
  "invalid": {
    "status": "error",
    "message": "Event type not registered"
  }
}
  • completed and totalCount are individually optional. It is valid to provide both in one update, or just one, but at least one must be provided per event type.
  • If one of the values is invalid, the others will still be updated, with partial errors returned on a 200 response.

When eventsCompleted becomes equal to eventsTotalCount for every custom event type, and all existing Atlassian-tracked events are also completed (including by acknowledging app data files and any Simplified App Migration event files), the transfer would be settled as SUCCESS.

Note: This does not provide a way to mark a custom event as FAILED - if this is a possibility, apps should still reserve one app data file to mark as FAILED when they are ready to fail the entire transfer.

Alternative we’re considering - tweaking timeouts

An alternative that we have considered is continuing to adjust the timeouts to a larger value. However, there is no timeout threshold that guarantees an app data file will always finish processing while remaining short enough to prevent customers from assuming a transfer has stalled.

Future extensions

Part of the reason for proposing a short description for each event type is so, in the future, we’ll be able to surface custom event status directly to customers (instead of just a percentage). This will provide additional transparency to customers (meaning descriptions must be clear as customer-facing strings, and the reported counts should be intuitive).

Asks

Atlassian has not yet decided whether we should implement this. A major deciding factor is how many apps would likely adopt the new APIs if we were to offer them.

We’d love to know:

  • If we were to implement this solution as is, would you find it helpful for any of your apps? If so, how quickly would you be likely to adopt it?
  • Is there anything we could change about the proposed solution that would make it more suitable for your apps?
  • If we implemented this, would you be better placed to guarantee some reported forward progress every 15 minutes, or would a longer timeout be necessary? If longer, what maximum interval would you suggest between updates so that your app could reliably report forward progress (assuming it is not genuinely stalled or failing)?
  • Is there an alternative timeout interval that would work for you with app data even if we didn’t make this change?

If we were to implement this solution as is, would you find it helpful for any of your apps? If so, how quickly would you be likely to adopt it?

The RFC description misses a few details, which makes it hard to judge how easy it would be to use. Assuming reasonable resolutions of those issues, we probably would have used it the when first implementing migrations. But we have already implemented migrations; the immediate benefits of the RFC would be limited, so we would not be in much of a hurry to use it. But see the next question.

One thing that is unclear on this RFC is who is going to do the REST calls for the custom events. The usual workflow is that the Server/DC app is triggered via the DiscoverableForgeListener API, and it sends events to the Cloud app via the AppCloudForgeMigrationGateway. The RFC is specified in terms of REST endpoints, which are what the Cloud app usually calls. This seems weird, because a straight reading of the RFC suggests that the DC app would need to wait for the Cloud app to notify Jira about the custom event types (which it has no synchronization method for) before being able to send them. Also, it is the Server/DC app that knows/can compute how much data it needs to send, so it seems like it would need to send its estimate to the Cloud and then the Cloud app would propagate them to Jira.

Alternatively, maybe you mean that the Server/DC app would need to do REST calls during the migration to tell Jira about custom events? But that is also weird, half the server-side operations migration would happen via the AppCloudForgeMigrationGateway and half via raw REST calls.

Is there anything we could change about the proposed solution that would make it more suitable for your apps?

IMO the main flaw of this RFC (and the JCMA migration process in general, both in Forge and Connect) is that it conflates two different “completion percentages”:

  1. Work progress: how much of the migration work has been done and how much time it’ll take to finish.
  2. Success ratio: how many of the items that should be migrated have actually been transferred to the cloud.

The confusion between these two concepts is by far the largest source of frustration and problems that customers encounter.

Imagine that I had an application that contains gizmos. A customer has one million gizmos on their Server/DC Jira instance, and wants to migrate them to the Cloud version. The main problem today is that there is no great way for apps to provide distinguishable “completion” percentages and statuses for the following fractions:

  • The Server/DC intended to upload only 900k gizmos (90%) because the other 100k depend on some Jira project that has not been migrated yet.
  • The Server/DC uploaded 800k gizmos but then crashed (~80%, failed);
  • The Cloud app consumed 700k of those uploaded gizmos (70%) and is still working on the next 200k (in progress);
  • 500k of the processed gizmos have been created in Cloud (50% success) but 150k were not migrated at all due to some issue (15% unsuccessful) and 50k were recreated on Cloud but only partially because some feature is missing (5% partial);
  • 300k of those 500k already existed in cloud from a previous migration, so completion percentage got to 30% instantly, after only 0% of this migration’s work/duration.

The customer can be interested in any of those numbers for different reasons. A migration that crashes after 50% of the work is very different from a migration that completed all the work but could not migrate 50% of the items because, say, the customer did not choose to migrate a required dependency.

The migration status being limited to “succeeded/failed” is also confusing for customers. If the app migrated 999k out of 1M gizmos, was that a success or a failure? Different customers interpret this differently.

If we implemented this, would you be better placed to guarantee some reported forward progress every 15 minutes, or would a longer timeout be necessary? If longer, what maximum interval would you suggest between updates so that your app could reliably report forward progress (assuming it is not genuinely stalled or failing)?

No. This RFC does not actually change anything about how the app must work around the 15 minutes limit, it just asks us to emit different event types, with the same time limits. There is no functional difference between sending a custom:app:is:not:dead event on a timer versus sending avi:ecosystem.migration:uploaded:app_data with a label like dummy-app-is-not-dead-yet.

Is there an alternative timeout interval that would work for you with app data even if we didn’t make this change?

I think one issue is that the migration process has “process event” as the only primitive operation.

You could just add something like AppCloudForgeMigrationGateway.logProgressMessage, for the Server/DC side of the app, and something like POST/app/migration/forge/v1/log-progress/{transferId}, which would just let the app say “I’m working on something, not stuck”, without imposing arbitrary 15 minutes limits on how long it should take to process each block of data.

Hi! Just to clarify this part first - the intention is that this would be called from a Forge Remote app or Container (and we’d provide an equivalent TypeScript / JavaScript API for Forge Functions), and that apps would declare the custom events at the start of the import, rather than the export (e.g. in response to the first app:data).

We could similarly open it up to the DC app via the Java API as an alternative way, if this would be valuable for many apps.

I think that the difficulty from the current solution right now is that you can report an app data as failed, but it immediately settles the migration as failed.

I agree that for apps where: a) some customers have some level of tolerance for a small amount of missing data due to historical problems / data quality, b) customers would like to complete as much of the migration as possible and then remediate, rather than stop after each failure, there is value in having a way to report early that something failed, while continuing to progress.

I think the current solution is actually extensible to that in the future - you could have custom event types that represent, for example, validation of data integrity. If we added an optional ‘failed’ count alongside completed (defaulting to zero when absent). We could make it so reporting a failure does not immediately settle the migration as failed, but will result in the migration eventually settling as failed when it does settle. We could even allow for remediation - some action that decreases the failed count and increases completed during the migration.

Realistically we’d probably have to improve things step-by-step, but opening up future options like this are certainly useful.

We do actually have this capability as an ‘escape hatch’ as a temporary solution for apps migrating from Connect, to buy them more time to implement better progress reporting. However, I don’t think it’s a good long-term solution (at least, not if we generalise the 15 minutes to an arbitrary longer window - we are open to feedback if you think 15 minutes should be longer). Let’s say for example, your app goes for 10 hours without anything other than “I’m working on something” - there is a good chance by then your customers are going to be raising urgent support tickets with both your team and Atlassian. If on the other hand, you estimated up front you have 1000 units of work, based on the size of the migration, and you’ve been regularly ticking up a number of units done which is now at 900, the customer will be a lot calmer.

We are proposing to allow you to update the 1000 units of work if you discover it’s actually 1,100. Units of work can even be minutes if you want - you’d just need some reasonable way of estimating how long it is (and regularly re-estimating how much longer if things are slower than planned). What you’d want to avoid is, for example, repeatedly saying ‘just one more minute’ (the system wouldn’t prevent that, but it would annoy customers) - but I acknowledge that could be hard if, say, it’s a big database query and you genuinely don’t know how long it will take.

Thanks for putting this up. It’s worth answering the adoption question, because our case differs from the one described above.

Our import keeps writing for hours after the last file has been uploaded, so we pre-upload a batch of dummy app data files and acknowledge them slowly to stop the transfer from looking stalled. That is the workaround your problem statement describes. It works, but the reserve has a hard ceiling that does not grow with the size of the migration, and the failure mode is what worries us. If the reserve runs out, or a single acknowledgement fails, the customer is shown a failed transfer, or one that timed out, for a migration whose data actually arrived complete. A wrong status on a correct migration is expensive to explain.

So yes, this would be useful to us, and it would not be a large change on our side: we would be replacing the reserve rather than adding a mechanism beside it. Having the migration already built is the reason we want it, not an argument against, because the reserve is the least reliable part of our import today.

The earlier reply is right that for a heartbeat on a timer this changes nothing about the 15 minute limit. For us the difference is the ceiling rather than the window: a progress update has no total limit, while a reserve of files does. A longer timeout would not help us for the same reason, which answers your last question as well.

One caveat on guaranteeing progress every 15 minutes. Parts of our import legitimately make no countable progress for a while, waiting for the remaining rows to arrive or on a lock while another batch writes. A heartbeat covers that, but a counter that must increase does not. We could declare a second event type measured in something that always moves, or in minutes as you suggest. Is that the intended pattern, or would you consider an explicit “still working” update that resets the timeout without claiming progress?

Also, you mentioned an escape hatch that already exists for apps migrating from Connect. Is that available to Forge apps today?

Thank you for your response - noted.

The escape hatch functionality is something you have to apply to Atlassian for, provide a reason / why it is temporary, and renew regularly (because it’s a bad customer experience due to migrations appearing stalled). If the reason for needing it is because of limitations on the Atlassian side or you haven’t had enough time since moving from Connect to Forge / are working on something to fix it, but need more time, it is likely to be approved. To get access, you’d need to open an ECOHELP ticket (making it clear this is about App Migrations, and the case for why you temporarily need it).

I’m hoping that if we build something like in the RFC, we’d remove the need for the escape hatch long term.

I think the proposed solution would be very helpful for our apps migrations. Our JCMA migration is multi-phase: we acknowledge app data early, then do longer async work (actions import + workflow rule migration via Jira APIs). Today platform progress is driven mainly by app-data acknowledgements, while real work continues afterward - migration.addLog() helps in logs but doesn’t move platform progress or reset timeouts. Custom event types would fit this well. We’d likely adopt in the release after GA, assuming the API is stable.

Suggestions to make it more suitable for our apps:

  • Maybe it could separate work progress from success ratio? “900/1000 workflow rules processed” is different from “900 migrated successfully.” Our app often finishes with items that need post-migration attention, so progress should reflect work completed, not imply full success.
  • Can it support heartbeat updates? Some steps (e.g. a single large workflow bulk operation) can run for a long time with no countable completed increment. An explicit “still working” signal, or a minutes-based event type, would cover those gaps.
  • Maybe it could also add some short descriptions per event type? For example “Workflow rules migrated”. This would help the admins understand the progress of migration better than just percentage alone.

Regarding progress every 15 minutes:
Usually yes for countable phases (we already report per workflow batch). But not always - some steps may run for a while without a meaningful completed increment. A heartbeat-style update would help there. Without custom events: no, especially after early app-data ack.

Would a longer timeout alone be enough? I don’t think so. It doesn’t fix misleading progress or post-ack async work. Maybe it would be useful as a fallback (for example 60–90 min), but not as the main solution.

Thank you for your helpful feedback Piotr - point taken about the value of being able to capture and surface early that there were partial failures but it is still worth continuing the migration.

We’d rather make a few small steps forward at a time, so we’d probably not do this as part of a first custom progress implementation, but I’ve noted that this would be a high value follow-up for Appfire, and we’ve heard similar from another app partner too.

Hi team,

Thank you for publishing the proposal! We definitely would like to try this new API once it’s released. A couple of questions:

  1. When you say that “apps would declare the custom events at the start of the import, rather than the export (e.g. in response to the first app:data)” do you mean that the migration event listener function could return a value with the custom event declarations or that it would need to call some new API that would do a POST /app/migration/forge/v1/customeventtypes/declare request internally? I’m little worried because there is a strict timeout for event trigger functions (25s I believe) and adding an additional REST call would reduce the amount of time left to perform meaningful work in the trigger.
  2. If an app declares custom event types will the mapping be available for download after all the app files are marked as processed but there is still uncompleted custom events left? This is quite crucial for us because our app downloads a lot of DC to Cloud mappings, it takes quite a long time and we’re currently use a hack where we deliberately delay marking app files as processed while mappings download is still in progress. We could really benefit from declaring a custom event type for mappings downloading because we could then get rid of the hack completely and report our progress much more transparently to users.

Hi @pmedyankin, thanks for your reply.

All of the cloud-side APIs can be called from Forge Functions, Forge Containers, or from outside of Atlassian Cloud as part of an application with Forge Remotes. That URL is where you’d call it from if you were calling from a non container / function. However, there’d be an equivalent TypeScript / JavaScript function to call from a Forge Function, and from a Container you’d call via the sidecar.

If you’re calling it from the Forge Function, you’d still be subject to the invocation time limit calling custom progress API. However, if you can consistently process one app data file in 25 seconds, you wouldn’t actually need custom progress, as presumably you’re seeing progress work well already with the default tracking. If you can’t, you’re likely already using scheduled triggers or dispatching async events, both of which allow timeouts to be configured up to 900 seconds on Forge Functions. You’d be able to call the custom progress API from these scheduled triggers or async event handlers, you wouldn’t be restricted to only calling them from the initial app event trigger handler.

Yes, the custom events are still in progress (and nothing is failed), the transfer would also stay as in progress, even if all app data events are acknowledged as successful.

However, the caveat with the current proposal is that there would be no way for the migration to fail (except through timeout) from custom events alone, so you may still want to hold back one app data event or Simplified App Migrations event file to allow you to mark the migration as failed if that’s a possibility during the mapping stage. However, you wouldn’t need to send extra app data to keep the migration alive, just one to mark the entire migration are failed in case that’s needed.

For example, you might use Simplified App Migration | App migration platform to get an export of all the mappings, and based on the number of event files, estimate the total amount of work, and declare custom events. And then as your import of mappings progresses, you’d send back custom event progress updates. In this case, you’d probably be best to fail one of the Simplified App Migration mapping event files if you found an unrecoverable problem processing it, causing the overall app data movement to fail.

Hope that answers your question.

At the moment, progress updates for the end user with JCMA happens this way:

  • Export/upload phase: 0%
  • Import phase: based on the number of files processed by the app vs total number of files uploaded.

The proposal helps to make the import phase progress a little more precise. From our view, this is a nice-to-have. Large migrations have enough files to have a progress granular enough with the current mechanism.

The proposal helps in terms of having a “heartbeat” mechanism, in case a file isn’t processed within 15 minutes (very unlikely anyway). However, I believe that there should be a better and more explicit heartbeat mechanism, such as a healthcheck endpoint that apps could implement.

In summary, the proposal looks like a nice-to-have, and it’s unlikely that we would use it for Zephyr.

Here are a few improvements we believe would add value to the proposed change:

  • A message for status updates: this was possible with Connect. Once the app sends an update, a place for sending a message e.g. “importing test cases X/Y” would be very helpful and provide good context for users.
  • A mechanism for the DC to send progress updates, so users won’t see only 0% until the export/upload finishes. With a message, so they can also see what is actually going on under the hood.