diff --git a/flamenco_worker/runner.py b/flamenco_worker/runner.py
index a1027eeb5fe8eb59d34c0a816a8c7197c44d154c..03a2f28d063ef5d3089df72b5bcee114cc06b604 100644
--- a/flamenco_worker/runner.py
+++ b/flamenco_worker/runner.py
@@ -356,6 +356,40 @@ class MoveOutOfWayCommand(AbstractCommand):
         src.rename(dst)
 
 
+@command_executor('move_to_final')
+class MoveToFinalCommand(AbstractCommand):
+    def validate(self, settings: dict):
+        _, err1 = self._setting(settings, 'src', True)
+        _, err2 = self._setting(settings, 'dest', True)
+        return err1 or err2
+
+    async def execute(self, settings: dict):
+        src = Path(settings['src'])
+        if not src.exists():
+            msg = 'Path %s does not exist, not moving' % src
+            self._log.info(msg)
+            await self.worker.register_log('%s: %s', self.command_name, msg)
+            return
+
+        dest = Path(settings['dest'])
+        if dest.exists():
+            backup = _timestamped_path(dest)
+            self._log.debug('Destination %s exists, moving out of the way to %s', dest, backup)
+
+            if backup.exists():
+                self._log.debug('Destination %s exists, finding one that does not', backup)
+                backup = _unique_path(backup)
+                self._log.debug('New destination is %s', backup)
+
+            self._log.info('Moving %s to %s', dest, backup)
+            await self.worker.register_log('%s: Moving %s to %s', self.command_name, dest, backup)
+            dest.rename(backup)
+
+        self._log.info('Moving %s to %s', src, dest)
+        await self.worker.register_log('%s: Moving %s to %s', self.command_name, src, dest)
+        src.rename(dest)
+
+
 @attr.s
 class AbstractSubprocessCommand(AbstractCommand):
     readline_timeout = attr.ib(default=SUBPROC_READLINE_TIMEOUT)
diff --git a/tests/test_runner_move_to_final.py b/tests/test_runner_move_to_final.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe5b3ce15f8ad013b0d6481c78c9415bb98483da
--- /dev/null
+++ b/tests/test_runner_move_to_final.py
@@ -0,0 +1,94 @@
+import os
+from pathlib import Path
+
+from test_runner import AbstractCommandTest
+
+
+class MoveToFinalTest(AbstractCommandTest):
+    def setUp(self):
+        super().setUp()
+
+        from flamenco_worker.runner import MoveToFinalCommand
+        import tempfile
+
+        self.tmpdir = tempfile.TemporaryDirectory()
+        self.tmppath = Path(self.tmpdir.name)
+
+        self.cmd = MoveToFinalCommand(
+            worker=self.fworker,
+            task_id='12345',
+            command_idx=0,
+        )
+        self.cmd.__attrs_post_init__()
+
+    def tearDown(self):
+        super().tearDown()
+        del self.tmpdir
+
+    def test_nonexistant_source(self):
+        src = self.tmppath / 'nonexistant-dir'
+        dest = self.tmppath / 'dest'
+
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        # Should be fine.
+        self.assertTrue(ok)
+        self.assertFalse(src.exists())
+        self.assertFalse(dest.exists())
+
+    def test_existing_source_and_dest(self):
+        src = self.tmppath / 'existing-dir'
+        src.mkdir()
+        (src / 'src-contents').touch()
+
+        # Make sure that the destination already exists, with some contents.
+        dest = self.tmppath / 'dest'
+        dest.mkdir()
+        (dest / 'dest-contents').touch()
+        (dest / 'dest-subdir').mkdir()
+        (dest / 'dest-subdir' / 'sub-contents').touch()
+
+        os.utime(str(dest), (1330712280, 1330712292))  # fixed (atime, mtime) for testing
+
+        # Run the command.
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+        self.assertTrue(ok)
+
+        renamed_dest = dest.with_name('dest-2012-03-02_191812')
+
+        # old 'dest' contents should exist at 'renamed_dest'
+        self.assertTrue(renamed_dest.exists())
+        self.assertTrue((renamed_dest / 'dest-contents').exists())
+        self.assertTrue((renamed_dest / 'dest-subdir').exists())
+        self.assertTrue((renamed_dest / 'dest-subdir' / 'sub-contents').exists())
+
+        # old 'src' contents should exist at 'dest'
+        self.assertTrue(dest.exists())
+        self.assertTrue((dest / 'src-contents').exists())
+
+        # old 'src' should no longer exist.
+        self.assertFalse(src.exists())
+
+    def test_nonexistant_dest(self):
+        src = self.tmppath / 'existing-dir'
+        src.mkdir()
+        (src / 'src-contents').touch()
+
+        dest = self.tmppath / 'dest'
+        self.assertFalse(dest.exists())
+
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+        self.assertTrue(ok)
+
+        # 'dest-{timestamp}' shouldn't exist.
+        self.assertFalse(list(self.tmppath.glob('dest-*')))
+
+        # old 'src' contents should exist at 'dest'
+        self.assertTrue(dest.exists())
+        self.assertTrue((dest / 'src-contents').exists())
+
+        # old 'src' should no longer exist.
+        self.assertFalse(src.exists())