diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37b09781901c5fff22ff426515fc04a1fd7ad6b2..1f8930737b69bbdf0b84e4a190dc575b05734c6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ changed functionality, fixed bugs).
 - Changed how progressive rendering works. Nonuniform tasks are now supported. This requires
   Flamenco Server 2.2 or newer. Progressive render jobs generated by older versions of Flamenco
   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.
 
 
 ## Version 2.2.1 (2019-01-14)
diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py
index f777ff5e33f76ea05a0311dc90b087e904aa2052..101779fd8fa145a7c93e827fc912f1c0dde5354a 100644
--- a/flamenco_worker/commands.py
+++ b/flamenco_worker/commands.py
@@ -1057,6 +1057,43 @@ class BlenderRenderAudioCommand(BlenderRenderCommand):
         ]
 
 
+@command_executor('exr_sequence_to_jpeg')
+class EXRSequenceToJPEGCommand(BlenderRenderCommand):
+    """Convert an EXR sequence to JPEG files.
+
+    This assumes the EXR files are named '{frame number}.exr', where the
+    frame number may have any number of leading zeroes.
+    """
+    pyscript = Path(__file__).parent / 'resources/exr_sequence_to_jpeg.py'
+
+    def validate(self, settings: Settings) -> typing.Optional[str]:
+        if not self.pyscript.exists():
+            raise FileNotFoundError(f'Resource script {self.pyscript} cannot be found')
+
+        exr_directory, err = self._setting(settings, 'exr_directory', True)
+        if err:
+            return err
+        if not exr_directory:
+            return '"exr_directory" may not be empty'
+
+        output_pattern, err = self._setting(settings, 'output_pattern', False,
+                                            default='preview-######.jpg')
+        if not output_pattern:
+            return '"output_pattern" may not be empty'
+        return super().validate(settings)
+
+    async def _build_blender_cmd(self, settings) -> typing.List[str]:
+        cmd = await super()._build_blender_cmd(settings)
+
+        return cmd + [
+            '--python-exit-code', '32',
+            '--python', str(self.pyscript),
+            '--',
+            '--exr-dir', settings['exr_directory'],
+            '--output-pattern', settings['output_pattern'],
+        ]
+
+
 class AbstractFFmpegCommand(AbstractSubprocessCommand, abc.ABC):
     index_file: typing.Optional[pathlib.Path] = None
 
diff --git a/flamenco_worker/resources/exr_sequence_to_jpeg.py b/flamenco_worker/resources/exr_sequence_to_jpeg.py
new file mode 100644
index 0000000000000000000000000000000000000000..36466f9c79c659a77a82663a5c92715b7c708e4d
--- /dev/null
+++ b/flamenco_worker/resources/exr_sequence_to_jpeg.py
@@ -0,0 +1,52 @@
+# This file is supposed to run inside Blender:
+# blender thefile.blend --python /path/to/exr_sequence_to_jpeg.py -- --exr-dir /path/to/exr/files
+
+import argparse
+import pathlib
+import sys
+
+import bpy
+
+# Find the EXR files to process.
+dashdash_index = sys.argv.index('--')
+parser = argparse.ArgumentParser()
+parser.add_argument('--exr-dir')
+parser.add_argument('--output-pattern')
+cli_args, _ = parser.parse_known_args(sys.argv[dashdash_index + 1:])
+
+imgdir = pathlib.Path(cli_args.exr_dir)
+exr_files = list(imgdir.glob('*.exr'))
+
+if not exr_files:
+    raise ValueError(f'No *.exr files found in {cli_args.exr_dir}')
+
+# Create a copy of the scene without data, so we can fill the sequence editor
+# with an image sequence.
+bpy.ops.scene.new(type='EMPTY')
+
+scene = bpy.context.scene
+se = scene.sequence_editor_create()
+
+# Place files at the correct frame, based on their filename.
+# This makes the rendering consistent w.r.t. gaps in the frames.
+# This assumes the files are named '000020.exr' etc.
+min_frame = float('inf')
+max_frame = float('-inf')
+for file in exr_files:
+    frame_num = int(file.stem, 10)
+    min_frame = min(min_frame, frame_num)
+    max_frame = max(max_frame, frame_num)
+    se.sequences.new_image(file.name, str(file), 1, frame_num)
+
+scene.frame_start = min_frame
+scene.frame_end = max_frame
+
+render = scene.render
+render.use_sequencer = True
+render.filepath = str(imgdir / cli_args.output_pattern)
+render.image_settings.file_format = 'JPEG'
+render.image_settings.quality = 90
+render.use_overwrite = True  # overwrite lesser quality previews
+
+bpy.ops.render.render(animation=True, use_viewport=False)
+bpy.ops.wm.quit_blender()
diff --git a/setup.py b/setup.py
index 7b01c68982bb3b1d8be85ac647419310b8f189eb..9255a8e5b6febfc31b49a949cda230d2395d24d5 100755
--- a/setup.py
+++ b/setup.py
@@ -57,6 +57,7 @@ class ZipCommand(Command):
             add_to_root(Path('README.md'))
             add_to_root(Path('CHANGELOG.md'))
             add_to_root(Path('flamenco_worker/resources/merge-exr.blend'))
+            add_to_root(Path('flamenco_worker/resources/exr_sequence_to_jpeg.py'))
 
             paths = collections.deque([Path('system-integration')])
             while paths: