diff --git a/toolkit/components/taskscheduler/TaskScheduler.sys.mjs b/toolkit/components/taskscheduler/TaskScheduler.sys.mjs
index d432484a0664502448d9776ea23e8f4c53f43fee..5e01932dfa61b0103e3516722119bb243ef7aa03 100644
--- a/toolkit/components/taskscheduler/TaskScheduler.sys.mjs
+++ b/toolkit/components/taskscheduler/TaskScheduler.sys.mjs
@@ -77,32 +77,40 @@ export var TaskScheduler = {
    * @param intervalSeconds
    *        Interval at which to run the command, in seconds. Minimum 1800 (30 minutes).
    *
-   * @param options
+   * @param {Object} options
    *        Optional, as are all of its properties:
    *        {
-   *          args
+   *          options.args
    *            Array of arguments to pass on the command line. Does not include the command
    *            itself even if that is considered part of the command line. If missing, no
    *            argument list is generated.
    *
-   *          workingDirectory
+   *          options.workingDirectory
    *            Working directory for the command. If missing, no working directory is set.
    *
-   *          description
+   *          options.description
    *            A description string that will be visible to system administrators. This should
    *            be localized. If missing, no description is set.
    *
-   *          disabled
+   *          options.disabled
    *            If true the task will be created disabled, so that it will not be run.
    *            Ignored on macOS: see comments in TaskSchedulerMacOSImpl.jsm.
    *            Default false, intended for tests.
    *
-   *          executionTimeoutSec
+   *          options.executionTimeoutSec
    *            Specifies how long (in seconds) the scheduled task can execute for before it is
    *            automatically stopped by the task scheduler. If a value <= 0 is given, it will be
    *            ignored.
    *            This is not currently implemented on macOS.
    *            On Windows, the default timeout is 72 hours.
+   *
+   *          options.nameVersion
+   *            Over time, we have needed to change the name format that tasks are registered with.
+   *            When interacting with an up-to-date task, this value can be unspecified and the
+   *            current version of the name format will be used by default. When interacting with
+   *            an out-of-date task using an old naming format, this can be used to specify what
+   *            version of the name should be used. Since the precise naming format is platform
+   *            specific, these version numbers are also platform-specific.
    *        }
    * }
    */
@@ -123,14 +131,37 @@ export var TaskScheduler = {
   /**
    * Delete a scheduled task previously created with registerTask.
    *
+   * @param {Object} options
+   *        Optional, as are all of its properties:
+   *        {
+   *            options.nameVersion
+   *              Over time, we have needed to change the name format that tasks are registered with.
+   *              When interacting with an up-to-date task, this value can be unspecified and the
+   *              current version of the name format will be used by default. When interacting with
+   *              an out-of-date task using an old naming format, this can be used to specify what
+   *              version of the name should be used. Since the precise naming format is platform
+   *              specific, these version numbers are also platform-specific.
+   *        }
    * @throws NS_ERROR_FILE_NOT_FOUND if the task does not exist.
    */
-  async deleteTask(id) {
-    return lazy.gImpl.deleteTask(id);
+  async deleteTask(id, options) {
+    return lazy.gImpl.deleteTask(id, options);
   },
 
   /**
    * Delete all tasks registered by this application.
+   *
+   * @param {Object} options
+   *        Optional, as are all of its properties:
+   *        {
+   *            options.nameVersion
+   *              Over time, we have needed to change the name format that tasks are registered with.
+   *              When interacting with an up-to-date task, this value can be unspecified and the
+   *              current version of the name format will be used by default. When interacting with
+   *              an out-of-date task using an old naming format, this can be used to specify what
+   *              version of the name should be used. Since the precise naming format is platform
+   *              specific, these version numbers are also platform-specific.
+   *        }
    */
   async deleteAllTasks() {
     return lazy.gImpl.deleteAllTasks();
@@ -142,10 +173,22 @@ export var TaskScheduler = {
    * @param id
    *        A string representing the identifier of the task to look for.
    *
+   * @param {Object} options
+   *        Optional, as are all of its properties:
+   *        {
+   *            options.nameVersion
+   *              Over time, we have needed to change the name format that tasks are registered with.
+   *              When interacting with an up-to-date task, this value can be unspecified and the
+   *              current version of the name format will be used by default. When interacting with
+   *              an out-of-date task using an old naming format, this can be used to specify what
+   *              version of the name should be used. Since the precise naming format is platform
+   *              specific, these version numbers are also platform-specific.
+   *        }
+   *
    * @return
    *        true if the task exists, otherwise false.
    */
-  async taskExists(id) {
-    return lazy.gImpl.taskExists(id);
+  async taskExists(id, options) {
+    return lazy.gImpl.taskExists(id, options);
   },
 };
diff --git a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs
index 682839068d3d5de3f3d820d64b71a7fbf99c8079..5ff9c09c58678487d9e76863f874cde6c37fac16 100644
--- a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs
+++ b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs
@@ -51,7 +51,7 @@ export var MacOSImpl = {
     let uid = await this._uid();
     lazy.log.debug(`registerTask: uid=${uid}`);
 
-    let label = this._formatLabelForThisApp(id);
+    let label = this._formatLabelForThisApp(id, options);
 
     // We ignore `options.disabled`, which is test only.
     //
@@ -126,10 +126,10 @@ export var MacOSImpl = {
     return true;
   },
 
-  async deleteTask(id) {
+  async deleteTask(id, options) {
     lazy.log.info(`deleteTask(${id})`);
 
-    let label = this._formatLabelForThisApp(id);
+    let label = this._formatLabelForThisApp(id, options);
     return this._deleteTaskByLabel(label);
   },
 
@@ -207,8 +207,8 @@ export var MacOSImpl = {
     lazy.log.debug(`deleteAllTasks: returning ${JSON.stringify(result)}`);
   },
 
-  async taskExists(id) {
-    const label = this._formatLabelForThisApp(id);
+  async taskExists(id, options) {
+    const label = this._formatLabelForThisApp(id, options);
     const path = this._formatPlistPath(label);
     return IOUtils.exists(path);
   },
@@ -292,12 +292,12 @@ export var MacOSImpl = {
     return serializer.serializeToString(doc);
   },
 
-  _formatLabelForThisApp(id) {
+  _formatLabelForThisApp(id, options) {
     let installHash = lazy.XreDirProvider.getInstallHash();
     return `${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.${id}`;
   },
 
-  _labelMatchesThisApp(label) {
+  _labelMatchesThisApp(label, options) {
     let installHash = lazy.XreDirProvider.getInstallHash();
     return (
       label &&
diff --git a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs
index b9f2716fb8d505aabd41b7031ec1f966c152210e..8d9c15c31494562a1080ff3b2291375319e0be00 100644
--- a/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs
+++ b/toolkit/components/taskscheduler/TaskSchedulerWinImpl.sys.mjs
@@ -40,16 +40,16 @@ export var WinImpl = {
 
     lazy.WinTaskSvc.registerTask(
       this._taskFolderName(),
-      this._formatTaskName(id),
+      this._formatTaskName(id, options),
       xml,
       updateExisting
     );
   },
 
-  deleteTask(id) {
+  deleteTask(id, options) {
     lazy.WinTaskSvc.deleteTask(
       this._taskFolderName(),
-      this._formatTaskName(id)
+      this._formatTaskName(id, options)
     );
   },
 
@@ -112,7 +112,7 @@ export var WinImpl = {
     }
   },
 
-  taskExists(id) {
+  taskExists(id, options) {
     const taskFolderName = this._taskFolderName();
 
     let allTasks;
@@ -126,7 +126,7 @@ export var WinImpl = {
       throw ex;
     }
 
-    return allTasks.includes(this._formatTaskName(id));
+    return allTasks.includes(this._formatTaskName(id, options));
   },
 
   _formatTaskDefinitionXML(command, intervalSeconds, options) {
@@ -270,13 +270,47 @@ export var WinImpl = {
     };
   },
 
-  _formatTaskName(id) {
+  /**
+   * Formats a given task id according to one of two formats.
+   *
+   * @param id
+   *        A string representing the identifier of the task to format
+   *
+   * @param {Object} options
+   *        Optional, as are all of its properties:
+   *        {
+   *            options.nameVersion
+   *              Specifies whether to search for tasks using nameVersion 1
+   *              which is `${taskID} ${installHash}` or nameVersion 2 which is
+   *              `${taskID} ${currentUserSid} ${installHash}`. Defaults to nameVersion 2.
+   *        }
+   *
+   * @return
+   *        Formatted task name.
+   */
+  _formatTaskName(id, options) {
     const installHash = lazy.XreDirProvider.getInstallHash();
-    return `${id} ${installHash}`;
+    if (options?.nameVersion == 1) {
+      return `${id} ${installHash}`;
+    }
+    const currentUserSid = lazy.WinTaskSvc.getCurrentUserSid();
+    return `${id} ${currentUserSid} ${installHash}`;
   },
 
   _matchAppTaskName(name) {
     const installHash = lazy.XreDirProvider.getInstallHash();
     return name.endsWith(` ${installHash}`);
   },
+
+  _updateTaskNameFormat(id) {
+    const taskFolderName = this._taskFolderName();
+    const allTasks = lazy.WinTaskSvc.getFolderTasks(taskFolderName);
+    const taskNameV1 = this._formatTaskName(id, { nameVersion: 1 });
+    const taskNameV2 = this._formatTaskName(id, { nameVersion: 2 });
+    if (allTasks.includes(taskNameV1)) {
+      const taskXML = lazy.WinTaskSvc.getTaskXML(taskFolderName, taskNameV1);
+      lazy.WinTaskSvc.registerTask(taskFolderName, taskNameV2, taskXML, true);
+      lazy.WinTaskSvc.deleteTask(taskFolderName, taskNameV1);
+    }
+  },
 };
diff --git a/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
index 7a7c5747c0e814e081bbbc8e07f74c357c80c17d..7778360e7d5e122a8c837a009aa2779686d87a8f 100644
--- a/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
+++ b/toolkit/components/taskscheduler/nsIWinTaskSchedulerService.idl
@@ -59,6 +59,17 @@ interface nsIWinTaskSchedulerService : nsISupports
    */
   AString getTaskXML(in wstring aFolderName, in wstring aTaskName);
 
+  /**
+   * Gets the sid of the current user.
+   *
+   * @throws NS_ERROR_NOT_IMPLEMENTED If called on a non-Windows OS.
+   * @throws NS_ERROR_FAILURE         If the user token cannot be found.
+   * @throws NS_ERROR_ABORT           If converting the sid to a string fails.
+   *
+   * @returns                         The sid of the current user.
+   */
+  AString getCurrentUserSid();
+
   /**
    * Delete a task.
    *
diff --git a/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
index 21b2909805afb93a69619c4890d6f590c020b1f0..1efae5c349d11d8d5ed27d186ed7ecf1af88ee6e 100644
--- a/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
+++ b/toolkit/components/taskscheduler/nsWinTaskScheduler.cpp
@@ -7,6 +7,8 @@
 
 #include <windows.h>
 #include <comdef.h>
+#include <sddl.h>
+#include <securitybaseapi.h>
 #include <taskschd.h>
 
 #include "nsString.h"
@@ -114,6 +116,30 @@ nsWinTaskSchedulerService::GetTaskXML(const char16_t* aFolderName,
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsWinTaskSchedulerService::GetCurrentUserSid(nsAString& aUserSid) {
+#ifndef XP_WIN
+  return NS_ERROR_NOT_IMPLEMENTED;
+#else  // !XP_WIN
+  DWORD tokenLen;
+  LPWSTR stringSid;
+  BYTE tokenBuf[TOKEN_USER_MAX_SIZE];
+  PTOKEN_USER tokenInfo = reinterpret_cast<PTOKEN_USER>(tokenBuf);
+  BOOL success = GetTokenInformation(GetCurrentProcessToken(), TokenUser,
+                                     tokenInfo, sizeof(tokenBuf), &tokenLen);
+  if (!success) {
+    return NS_ERROR_FAILURE;
+  }
+  success = ConvertSidToStringSidW(tokenInfo->User.Sid, &stringSid);
+  if (!success) {
+    return NS_ERROR_ABORT;
+  }
+  aUserSid.Assign(stringSid);
+  LocalFree(stringSid);
+  return NS_OK;
+#endif
+}
+
 NS_IMETHODIMP
 nsWinTaskSchedulerService::RegisterTask(const char16_t* aFolderName,
                                         const char16_t* aTaskName,
diff --git a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js
index d0c2a6c357fa522edf3207694fa39c12c41b995c..ed1be3e49b4a274e9d882f3844edc194ac708e91 100644
--- a/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js
+++ b/toolkit/components/taskscheduler/tests/xpcshell/test_TaskSchedulerWinImpl.js
@@ -200,3 +200,92 @@ add_task(async function test_create() {
   );
   Assert.equal(WinSvc.validateTaskDefinition(basicXML), 0 /* S_OK */);
 });
+
+add_task(async function test_migrate() {
+  // Create task name with nameVersion1
+  const taskName = "test-task-1";
+  const rawTaskNameV1 = WinImpl._formatTaskName(taskName, { nameVersion: 1 });
+  const rawTaskNameV2 = WinImpl._formatTaskName(taskName, { nameVersion: 2 });
+  const folderName = WinImpl._taskFolderName();
+  const exePath = "C:\\Program Files\\XYZ\\123.exe";
+  const workingDir = "C:\\Program Files\\XYZ";
+  const argsIn = [
+    "x.txt",
+    "c:\\x.txt",
+    'C:\\"HELLO WORLD".txt',
+    "only space.txt",
+  ];
+  const expectedArgsOutStr = [
+    "x.txt",
+    "c:\\x.txt",
+    '"C:\\\\\\"HELLO WORLD\\".txt"',
+    '"only space.txt"',
+  ].join(" ");
+  const description = "Entities: < &. Non-ASCII: abc😀def.";
+  const intervalSecsIn = 2 * 60 * 60; // 2 hours
+  const expectedIntervalOut = "PT2H"; // 2 hours
+
+  const queries = [
+    ["Actions Exec Command", exePath],
+    ["Actions Exec WorkingDirectory", workingDir],
+    ["Actions Exec Arguments", expectedArgsOutStr],
+    ["RegistrationInfo Description", description],
+    ["RegistrationInfo Author", Services.appinfo.vendor],
+    ["Settings Enabled", "false"],
+    ["Triggers TimeTrigger Repetition Interval", expectedIntervalOut],
+  ];
+
+  await TaskScheduler.registerTask(taskName, exePath, intervalSecsIn, {
+    disabled: true,
+    args: argsIn,
+    description,
+    workingDirectory: workingDir,
+    nameVersion: 1,
+  });
+
+  ok(
+    WinImpl.taskExists(taskName, { nameVersion: 1 }),
+    "Task exists with nameVersion1"
+  );
+  const originalTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV1);
+  const parser = new DOMParser();
+  const docV1 = parser.parseFromString(originalTaskXML, "text/xml");
+
+  Assert.equal(docV1.documentElement.tagName, "Task");
+
+  // Check for the values set above
+  for (let [sel, expected] of queries) {
+    Assert.equal(
+      docV1.querySelector(sel).textContent,
+      expected,
+      `Task V1 ${sel} had expected textContent`
+    );
+  }
+
+  // Update task name format to nameVersion2
+  WinImpl._updateTaskNameFormat(taskName);
+  ok(
+    WinImpl.taskExists(taskName, { nameVersion: 2 }),
+    "Task exists with nameVersion2"
+  );
+  ok(
+    !WinImpl.taskExists(taskName, { nameVersion: 1 }),
+    "Task with nameVersion1 successfully deleted"
+  );
+
+  // Check that the new task XML is still valid
+  const newTaskXML = WinSvc.getTaskXML(folderName, rawTaskNameV2);
+  Assert.equal(WinSvc.validateTaskDefinition(newTaskXML), 0 /* S_OK */);
+  const docV2 = parser.parseFromString(newTaskXML, "text/xml");
+
+  Assert.equal(docV2.documentElement.tagName, "Task");
+
+  // Check that the updated values still match the provided ones.
+  for (let [sel, expected] of queries) {
+    Assert.equal(
+      docV2.querySelector(sel).textContent,
+      expected,
+      `Task V2 ${sel} had expected textContent`
+    );
+  }
+});
diff --git a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
index fd88a33dfcd9398e531e981228593112514d6e4b..70fc8388404910d012a42f635321969ef4db3f10 100644
--- a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
+++ b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
@@ -56,10 +56,31 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
 // by storing the installed version number of the task to a pref and comparing
 // that version number to the current version. If they aren't equal, we know
 // that we have to re-register the task.
-const TASK_DEF_CURRENT_VERSION = 3;
+const TASK_DEF_CURRENT_VERSION = 4;
 const TASK_INSTALLED_VERSION_PREF =
   "app.update.background.lastInstalledTaskVersion";
 
+// This returns the version of the task naming scheme being used which
+// is different from the task version used for the task definition.
+function taskNameVersion(taskVersion) {
+  if (AppConstants.platform != "win" || taskVersion < 4) {
+    return 1;
+  }
+  return 2;
+}
+
+async function deleteTasksInRange(installedVersion, currentVersion) {
+  for (
+    let taskVersion = installedVersion;
+    taskVersion <= currentVersion;
+    taskVersion++
+  ) {
+    await lazy.TaskScheduler.deleteTask(this.taskId, {
+      nameVersion: taskNameVersion(taskVersion),
+    });
+  }
+}
+
 export var BackgroundUpdate = {
   QueryInterface: ChromeUtils.generateQI([
     "nsINamed",
@@ -504,7 +525,11 @@ export var BackgroundUpdate = {
         );
 
         if (!successfullyReadPrevious || previousEnabled) {
-          await lazy.TaskScheduler.deleteTask(this.taskId);
+          let installedVersion = Services.prefs.getIntPref(
+            TASK_INSTALLED_VERSION_PREF,
+            TASK_DEF_CURRENT_VERSION
+          );
+          await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION);
           lazy.log.debug(
             `${SLUG}: witnessed falling (enabled -> disabled) edge; deleted task ${this.taskId}.`
           );
@@ -533,7 +558,11 @@ export var BackgroundUpdate = {
             `Removing task so the new version can be registered`
         );
         try {
-          await lazy.TaskScheduler.deleteTask(this.taskId);
+          let installedVersion = Services.prefs.getIntPref(
+            TASK_INSTALLED_VERSION_PREF,
+            TASK_DEF_CURRENT_VERSION
+          );
+          await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION);
         } catch (e) {
           lazy.log.error(`${SLUG}: Error removing old task: ${e}`);
         }