diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f8930737b69bbdf0b84e4a190dc575b05734c6f..963293c49377ab76eca24ce5d6fdd95b3d1fefbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ changed functionality, fixed bugs).
   Server are no longer supported.
 - Added the `exr_sequence_to_jpeg` command. This command uses Blender to convert a sequence of
   EXR files to JPEG files. This is used in progressive rendering to get intermediary previews.
+- Added the `merge_progressive_render_sequence` for sample-merging sequences of EXR files. The
+  already-existing `merge_progressive_renders` command only performed on one frame at a time.
 
 
 ## Version 2.2.1 (2019-01-14)
diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py
index e859e383668818b05eb5d68c21cf7fe14f3ab147..4f14095a4ac7418a89b41682fd0d09b3ad13c1e4 100644
--- a/flamenco_worker/commands.py
+++ b/flamenco_worker/commands.py
@@ -45,14 +45,52 @@ nodes["image2"].image = image2
 nodes["weight1"].outputs[0].default_value = %(weight1)i
 nodes["weight2"].outputs[0].default_value = %(weight2)i
 
-nodes["output"].base_path = "%(tmpdir)s"
 scene.render.resolution_x, scene.render.resolution_y = image1.size
 scene.render.tile_x, scene.render.tile_y = image1.size
-scene.render.filepath = "%(tmpdir)s/preview.jpg"
+scene.render.filepath = "%(tmpdir)s/merged0001.exr"
 
+scene.frame_start = 1
+scene.frame_end = 1
+scene.frame_set(1)
 bpy.ops.render.render(write_still=True)
 """
 
+MERGE_EXR_SEQUENCE_PYTHON = """\
+import bpy
+scene = bpy.context.scene
+nodes = scene.node_tree.nodes
+
+image1 = bpy.data.images.load("%(input1)s")
+image2 = bpy.data.images.load("%(input2)s")
+image1.source = 'SEQUENCE'
+image2.source = 'SEQUENCE'
+
+node_img1 = nodes["image1"]
+node_img2 = nodes["image2"]
+node_img1.image = image1
+node_img2.image = image2
+node_img1.frame_duration = %(frame_end)d - %(frame_start)d + 1
+node_img2.frame_duration = %(frame_end)d - %(frame_start)d + 1
+node_img1.frame_start = %(frame_start)d
+node_img2.frame_start = %(frame_start)d
+node_img1.frame_offset = %(frame_start)d - 1
+node_img2.frame_offset = %(frame_start)d - 1
+
+nodes["weight1"].outputs[0].default_value = %(weight1)i
+nodes["weight2"].outputs[0].default_value = %(weight2)i
+
+scene.render.resolution_x, scene.render.resolution_y = image1.size
+scene.render.tile_x, scene.render.tile_y = image1.size
+scene.render.filepath = "%(output)s"
+
+scene.frame_start = %(frame_start)d
+scene.frame_end = %(frame_end)d
+
+bpy.ops.render.render(animation=True)
+"""
+
+HASHES_RE = re.compile('#+')
+
 log = logging.getLogger(__name__)
 
 
@@ -330,6 +368,15 @@ def _numbered_path(directory: Path, fname_prefix: str, fname_suffix: str) -> Pat
     return directory / f'{fname_prefix}{max_nr + 1:03}{fname_suffix}'
 
 
+def _hashes_to_glob(path: Path) -> Path:
+    """Transform bla-#####.exr to bla-*.exr.
+
+    >>> _hashes_to_glob(Path('/path/to/bla-####.exr'))
+    Path('/path/to/bla-*.exr')
+    """
+    return path.with_name(HASHES_RE.sub('*', path.name))
+
+
 @command_executor('move_out_of_way')
 class MoveOutOfWayCommand(AbstractCommand):
     def validate(self, settings: Settings):
@@ -918,6 +965,8 @@ class BlenderRenderProgressiveCommand(BlenderRenderCommand):
 
 @command_executor('merge_progressive_renders')
 class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
+    script_template = MERGE_EXR_PYTHON
+
     def validate(self, settings: Settings):
         blender_cmd, err = self._setting(settings, 'blender_cmd', True)
         if err:
@@ -928,20 +977,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
         settings['blender_cmd'] = cmd
 
         input1, err = self._setting(settings, 'input1', True, str)
-        if err: return err
+        if err:
+            return err
         if '"' in input1:
             return 'Double quotes are not allowed in filenames: %r' % input1
         if not Path(input1).exists():
             return 'Input 1 %r does not exist' % input1
         input2, err = self._setting(settings, 'input2', True, str)
-        if err: return err
+        if err:
+            return err
         if '"' in input2:
             return 'Double quotes are not allowed in filenames: %r' % input2
         if not Path(input2).exists():
             return 'Input 2 %r does not exist' % input2
 
         output, err = self._setting(settings, 'output', True, str)
-        if err: return err
+        if err:
+            return err
         if '"' in output:
             return 'Double quotes are not allowed in filenames: %r' % output
 
@@ -950,24 +1002,17 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
         settings['output'] = Path(output).as_posix()
 
         _, err = self._setting(settings, 'weight1', True, int)
-        if err: return err
+        if err:
+            return err
 
         _, err = self._setting(settings, 'weight2', True, int)
-        if err: return err
+        if err:
+            return err
 
         return super().validate(settings)
 
     async def execute(self, settings: Settings):
-        blendpath = Path(__file__).parent / 'resources/merge-exr.blend'
-
-        cmd = settings['blender_cmd'][:]
-        cmd += [
-            '--factory-startup',
-            '--enable-autoexec',
-            '-noaudio',
-            '--background',
-            blendpath.as_posix(),
-        ]
+        cmd = self._base_blender_cli(settings)
 
         # set up node properties and render settings.
         output = Path(settings['output'])
@@ -979,7 +1024,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
 
             settings['tmpdir'] = tmppath.as_posix()
             cmd += [
-                '--python-expr', MERGE_EXR_PYTHON % settings
+                '--python-expr', self.script_template % settings
             ]
 
             await self.worker.register_task_update(activity='Starting Blender to merge EXR files')
@@ -987,11 +1032,23 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
 
             # move output files into the correct spot.
             await self.move(tmppath / 'merged0001.exr', output)
-            # await self.move(tmppath / 'preview.jpg', output.with_suffix('.jpg'))
 
         # See if this line logs the saving of a file.
         self.worker.output_produced(output)
 
+    def _base_blender_cli(self, settings):
+        blendpath = Path(__file__).parent / 'resources/merge-exr.blend'
+
+        cmd = settings['blender_cmd'] + [
+            '--factory-startup',
+            '--enable-autoexec',
+            '-noaudio',
+            '--background',
+            blendpath.as_posix(),
+            '--python-exit-code', '47',
+        ]
+        return cmd
+
     async def move(self, src: Path, dst: Path):
         """Moves a file to another location."""
 
@@ -1006,6 +1063,44 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
         shutil.move(str(src), str(dst))
 
 
+@command_executor('merge_progressive_render_sequence')
+class MergeProgressiveRenderSequenceCommand(MergeProgressiveRendersCommand):
+    script_template = MERGE_EXR_SEQUENCE_PYTHON
+
+    def validate(self, settings: Settings):
+        err = super().validate(settings)
+        if err:
+            return err
+
+        if '##' not in settings['output']:
+            return 'Output filename should contain at least two "##" marks'
+
+        _, err = self._setting(settings, 'frame_start', True, int)
+        if err:
+            return err
+
+        _, err = self._setting(settings, 'frame_end', True, int)
+        if err:
+            return err
+
+    async def execute(self, settings: Settings):
+        cmd = self._base_blender_cli(settings)
+
+        # set up node properties and render settings.
+        output = Path(settings['output'])
+        await self._mkdir_if_not_exists(output.parent)
+
+        cmd += [
+            '--python-expr', self.script_template % settings
+        ]
+        await self.worker.register_task_update(activity='Starting Blender to merge EXR sequence')
+        await self.subprocess(cmd)
+
+        as_glob = _hashes_to_glob(output)
+        for fpath in as_glob.parent.glob(as_glob.name):
+            self.worker.output_produced(fpath)
+
+
 # TODO(Sybren): maybe subclass AbstractBlenderCommand instead?
 @command_executor('blender_render_audio')
 class BlenderRenderAudioCommand(BlenderRenderCommand):
diff --git a/flamenco_worker/resources/merge-exr.blend b/flamenco_worker/resources/merge-exr.blend
index 2dda49f47950de3bc5f9a6959b62dab5c3b27b59..c2904b068f393d9d8a54bdfcd4fe8d8dbfcdb5e5 100644
Binary files a/flamenco_worker/resources/merge-exr.blend and b/flamenco_worker/resources/merge-exr.blend differ
diff --git a/tests/test_commands_merge_exr.py b/tests/test_commands_merge_exr.py
index d34339852b974639795735ca5396ae9b42895adc..64455d427ddd85f051205b8d88aef8ce07606a5a 100644
--- a/tests/test_commands_merge_exr.py
+++ b/tests/test_commands_merge_exr.py
@@ -43,7 +43,47 @@ class MergeProgressiveRendersCommandTest(AbstractCommandTest):
         self.assertTrue(output.exists())
         self.assertTrue(output.is_file())
 
-        # Sybren disabled preview generation since we don't use those any more
-        # in the studio.
-        # self.assertTrue(output.with_suffix('.jpg').exists())
-        # self.assertTrue(output.with_suffix('.jpg').is_file())
+
+class MergeProgressiveRenderSequenceCommandTest(AbstractCommandTest):
+    def setUp(self):
+        super().setUp()
+
+        from flamenco_worker.commands import MergeProgressiveRenderSequenceCommand
+        import tempfile
+
+        self.tmpdir = tempfile.TemporaryDirectory()
+        self.mypath = Path(__file__).parent
+
+        self.cmd = MergeProgressiveRenderSequenceCommand(
+            worker=self.fworker,
+            task_id='12345',
+            command_idx=0,
+        )
+
+    def tearDown(self):
+        super().tearDown()
+        self.tmpdir.cleanup()
+
+    def test_happy_flow(self):
+        output = Path(self.tmpdir.name) / 'merged-samples-######.exr'
+
+        settings = {
+            'blender_cmd': self.find_blender_cmd(),
+            'input1': str(self.mypath / 'Corn field-1k.exr'),
+            'input2': str(self.mypath / 'Deventer-1k.exr'),
+            'weight1': 20,
+            'weight2': 100,
+            'output': str(output),
+            'frame_start': 3,
+            'frame_end': 5,
+        }
+
+        task = self.cmd.run(settings)
+        ok = self.loop.run_until_complete(task)
+        self.assertTrue(ok)
+
+        # Assuming that if the files exist, the merge was ok.
+        for framenr in range(3, 6):
+            framefile = output.with_name(f'merged-samples-{framenr:06}.exr')
+            self.assertTrue(framefile.exists(), f'cannot find {framefile}')
+            self.assertTrue(framefile.is_file(), f'{framefile} is not a file')