diff --git a/packages/flamenco/flamenco/job_compilers/commands.py b/packages/flamenco/flamenco/job_compilers/commands.py
index 45498c15c15b1d16af2cdc23c70be67d8a3365d5..40254d19f46a37a21deeedc5e9baee0959af693c 100644
--- a/packages/flamenco/flamenco/job_compilers/commands.py
+++ b/packages/flamenco/flamenco/job_compilers/commands.py
@@ -11,7 +11,9 @@ class AbstractCommand(object):
     @classmethod
     def cmdname(cls):
         """Returns the command name."""
-        return cls.__name__.lower()
+        from flamenco.utils import camel_case_to_lower_case_underscore
+
+        return camel_case_to_lower_case_underscore(cls.__name__)
 
     def to_dict(self):
         """Returns a dictionary representation of this command, for JSON serialisation."""
@@ -34,10 +36,6 @@ class Echo(AbstractCommand):
 
 @attr.s
 class BlenderRender(AbstractCommand):
-    @classmethod
-    def cmdname(cls):
-        return 'blender_render'
-
     # Blender executable to run.
     blender_cmd = attr.ib(validator=attr.validators.instance_of(unicode))
     # blend file path.
diff --git a/packages/flamenco/flamenco/utils.py b/packages/flamenco/flamenco/utils.py
index 3b663802942552f0e91b76991eb6ea2cf1e5efc9..94cdbf0447826283f97bca2b57104b7d37578b8f 100644
--- a/packages/flamenco/flamenco/utils.py
+++ b/packages/flamenco/flamenco/utils.py
@@ -103,3 +103,29 @@ def report_duration(logger, description):
     finally:
         end_time = time.time()
         logger.debug('report_duration: %s took %f seconds', description, end_time - start_time)
+
+
+def camel_case_to_lower_case_underscore(string):
+    """
+    Converts a string from CamelCase to lower_case_underscore.
+
+    None-safe.
+
+    :type string: unicode or str or None
+    :rtype: unicode or str or None
+    """
+
+    if string is None:
+        return None
+
+    words = []
+    from_char_position = 0
+    for current_char_position, char in enumerate(string):
+        if char.isupper() and from_char_position < current_char_position:
+            words.append(string[from_char_position:current_char_position].lower())
+            from_char_position = current_char_position
+    words.append(string[from_char_position:].lower())
+
+    if isinstance(string, str):
+        return '_'.join(words)
+    return u'_'.join(words)
diff --git a/packages/flamenco/tests/test_commands.py b/packages/flamenco/tests/test_commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..20887eef076e52a45ba6fb44f7fdc9b6989bf590
--- /dev/null
+++ b/packages/flamenco/tests/test_commands.py
@@ -0,0 +1,8 @@
+from unittest import TestCase
+
+
+class SomeCommandsTest(TestCase):
+    def test_blender_render_name(self):
+        from flamenco.job_compilers.commands import BlenderRender
+
+        self.assertEqual('blender_render', BlenderRender.cmdname())
diff --git a/packages/flamenco/tests/test_utils.py b/packages/flamenco/tests/test_utils.py
index 390e52fee503531994da8e08ad6fcab4c7f13818..34da6e36dbcf573f524fc70000f3beb5c38ff7fe 100644
--- a/packages/flamenco/tests/test_utils.py
+++ b/packages/flamenco/tests/test_utils.py
@@ -59,3 +59,17 @@ class FrameRangeTest(unittest.TestCase):
                 [14, 15, 16],
             ],
             list(iter_frame_range('4-10, 13-16', 4)))
+
+    def test_camel_case_to_lower_case_underscore(self):
+        from flamenco.utils import camel_case_to_lower_case_underscore as cctlcu
+
+        self.assertIsInstance(cctlcu(u'word'), unicode)
+        self.assertIsInstance(cctlcu('word'), str)
+
+        self.assertEqual('word', cctlcu('word'))
+        self.assertEqual(u'word', cctlcu(u'word'))
+        self.assertEqual('camel_case', cctlcu('CamelCase'))
+        self.assertEqual('camel_case', cctlcu('camelCase'))
+        self.assertEqual('camel_case_with_many_words', cctlcu('CamelCaseWithManyWords'))
+        self.assertEqual('', cctlcu(''))
+        self.assertIs(None, cctlcu(None))