diff --git a/README.md b/README.md
index 33c0168..307f04c 100644
--- a/README.md
+++ b/README.md
@@ -199,6 +199,8 @@ the full list:
 * `collect`: for scripts that should run only when all tasks from all their
   prerequisites have completed.
 * `dependencies`: a list of previous steps that a step depends on.
+* `error step`: if `true` the step script will only be run if one of its
+  dependencies fails.
 * `skip`: if `true`, the step script will be run with `SP_SKIP=1` in its
   environment. Otherwise, `SP_SKIP` will always be set and will be `0`.
 
diff --git a/setup.py b/setup.py
index 7971140..2071b15 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
 from setuptools import setup
 
 setup(name='slurm-pipeline',
-      version='1.0.13',
+      version='1.0.14',
       packages=['slurm_pipeline'],
       include_package_data=True,
       url='https://github.com/acorg/slurm-pipeline',
diff --git a/slurm_pipeline/pipeline.py b/slurm_pipeline/pipeline.py
index 713272d..8161b2c 100644
--- a/slurm_pipeline/pipeline.py
+++ b/slurm_pipeline/pipeline.py
@@ -159,6 +159,13 @@ class SlurmPipeline(object):
         step['skip'] = skip
         scriptArgsStr = ' '.join(map(str, scriptArgs)) if scriptArgs else ''
 
+        if step.get('error step', False):
+            separator = '?'
+            after = 'afternotok'
+        else:
+            separator = ','
+            after = 'afterok'
+
         # taskDependencies is keyed by task name. These are the tasks
         # started by the steps that the current step depends on.  Its
         # values are sets of SLURM job ids the tasks that step started and
@@ -178,8 +185,8 @@ class SlurmPipeline(object):
                 env['SP_ORIGINAL_ARGS'] = scriptArgsStr
                 env['SP_SIMULATE'] = str(int(simulate))
                 env['SP_SKIP'] = str(int(skip))
-                dependencies = ','.join(
-                    sorted(('afterok:%d' % jobId)
+                dependencies = separator.join(
+                    sorted(('%s:%d' % (after, jobId))
                            for jobIds in taskDependencies.values()
                            for jobId in jobIds))
                 if dependencies:
@@ -196,8 +203,9 @@ class SlurmPipeline(object):
                     env['SP_SIMULATE'] = str(int(simulate))
                     env['SP_SKIP'] = str(int(skip))
                     jobIds = steps[stepName]['tasks'][taskName]
-                    dependencies = ','.join(sorted(('afterok:%d' % jobId)
-                                                   for jobId in jobIds))
+                    dependencies = separator.join(
+                        sorted(('%s:%d' % (after, jobId))
+                               for jobId in jobIds))
                     if dependencies:
                         env['SP_DEPENDENCY_ARG'] = ('--dependency=' +
                                                     dependencies)
@@ -352,6 +360,11 @@ class SlurmPipeline(object):
                             'Step %d depends on a non-existent (or '
                             'not-yet-defined) step: %r' % (count, dependency))
 
+            if 'error step' in step and 'dependencies' not in step:
+                    raise SpecificationError(
+                        'Step "%s" is an error step but has no "dependencies" '
+                        'key' % (step['name']))
+
     def _checkRuntime(self, steps, firstStep=None, lastStep=None, skip=None):
         """
         Check that a proposed scheduling makes sense.

Add a way to have an error step
diff --git a/test/test_pipeline.py b/test/test_pipeline.py
index 4e0e396..0057a04 100644
--- a/test/test_pipeline.py
+++ b/test/test_pipeline.py
@@ -1491,3 +1491,76 @@ class TestRunner(TestCase):
             },
             sort_keys=True, indent=2, separators=(',', ': '))
         self.assertEqual(expected, runner.specificationToJSON(specification))
+
+    @patch('os.access')
+    @patch('os.path.exists')
+    def testErrorStepWithNoDependencies(self, existsMock, accessMock):
+        """
+        If the specification steps contains an error step that does not have
+        any dependencies, a SpecificationError must be raised because error
+        steps only exist for the purposed of catching failed dependencies.
+        """
+        error = '^Step "xx" is an error step but has no "dependencies" key$'
+        assertRaisesRegex(self, SpecificationError, error, SlurmPipeline,
+                          {
+                              'steps': [
+                                  {
+                                      'error step': True,
+                                      'name': 'xx',
+                                      'script': 'script',
+                                  },
+                              ]
+                          })
+
+    @patch('subprocess.check_output')
+    @patch('os.access')
+    @patch('os.path.exists')
+    def testErrorStep(self, existsMock, accessMock, subprocessMock):
+        """
+        If a step is an error step its script must be run with the expected
+        value of SP_DEPENDENCY_ARG.
+        """
+
+        class SideEffect(object):
+            def __init__(self):
+                self.first = True
+
+            def sideEffect(self, *args, **kwargs):
+                if self.first:
+                    self.first = False
+                    return ('TASK: aaa 127 450\n'
+                            'TASK: bbb 238 560\n')
+                else:
+                    return ''
+
+        subprocessMock.side_effect = SideEffect().sideEffect
+
+        runner = SlurmPipeline(
+            {
+                'steps': [
+                    {
+                        'name': 'name1',
+                        'script': 'script1',
+                    },
+                    {
+                        'dependencies': ['name1'],
+                        'error step': True,
+                        'name': 'name2',
+                        'script': 'script2',
+                    },
+                ],
+            })
+        runner.schedule()
+
+        # Check that the dependency environment variable is correct in
+        # all calls.
+        env1 = subprocessMock.mock_calls[0][2]['env']
+        self.assertNotIn('SP_DEPENDENCY_ARG', env1)
+
+        env2 = subprocessMock.mock_calls[1][2]['env']
+        self.assertEqual('--dependency=afternotok:127?afternotok:450',
+                         env2['SP_DEPENDENCY_ARG'])
+
+        env3 = subprocessMock.mock_calls[2][2]['env']
+        self.assertEqual('--dependency=afternotok:238?afternotok:560',
+                         env3['SP_DEPENDENCY_ARG'])

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements-3.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
discover==0.4.0
exceptiongroup==1.2.2
iniconfig==2.1.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
six==1.10.0
-e git+https://github.com/acorg/slurm-pipeline.git@3c7a4dbac953c478d6370af708b3642ef3e790b0#egg=slurm_pipeline
tomli==2.2.1

name: slurm-pipeline
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - discover==0.4.0
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - six==1.10.0
      - tomli==2.2.1
prefix: /opt/conda/envs/slurm-pipeline

test/test_pipeline.py::TestRunner::testErrorStep test/test_pipeline.py::TestRunner::testErrorStepWithNoDependencies
test/test_pipeline.py::TestRunner::testAccessAndExistsAreCalled test/test_pipeline.py::TestRunner::testCwdWithRelativeScriptPath test/test_pipeline.py::TestRunner::testDuplicateName test/test_pipeline.py::TestRunner::testEmptyJSON test/test_pipeline.py::TestRunner::testFirstStepAndLastStepDifferent test/test_pipeline.py::TestRunner::testFirstStepAndLastStepSame test/test_pipeline.py::TestRunner::testFirstStepOnly test/test_pipeline.py::TestRunner::testForce test/test_pipeline.py::TestRunner::testInvalidJSON test/test_pipeline.py::TestRunner::testJSON test/test_pipeline.py::TestRunner::testJSONList test/test_pipeline.py::TestRunner::testLastStepBeforeFirstStep test/test_pipeline.py::TestRunner::testLastStepOnly test/test_pipeline.py::TestRunner::testList test/test_pipeline.py::TestRunner::testNoSteps test/test_pipeline.py::TestRunner::testNonDictionaryStep test/test_pipeline.py::TestRunner::testNonExistentDependency test/test_pipeline.py::TestRunner::testNonListDependencies test/test_pipeline.py::TestRunner::testNonStringName test/test_pipeline.py::TestRunner::testNonStringScript test/test_pipeline.py::TestRunner::testNonYetDefinedDependency test/test_pipeline.py::TestRunner::testNonexistentFirstStep test/test_pipeline.py::TestRunner::testNonexistentLastStep test/test_pipeline.py::TestRunner::testNonexistentScript test/test_pipeline.py::TestRunner::testRepeatedTaskJobId test/test_pipeline.py::TestRunner::testRepeatedTaskName test/test_pipeline.py::TestRunner::testScheduledTime test/test_pipeline.py::TestRunner::testScriptArgs test/test_pipeline.py::TestRunner::testSingleCollectorDependencyTaskNamesAndJobIds test/test_pipeline.py::TestRunner::testSingleDependencySynchronousTaskNamesJobIdsAndCalls test/test_pipeline.py::TestRunner::testSingleDependencyTaskNamesJobIdsAndCalls test/test_pipeline.py::TestRunner::testSkipNone test/test_pipeline.py::TestRunner::testSkipNonexistentStep test/test_pipeline.py::TestRunner::testSkipNonexistentSteps test/test_pipeline.py::TestRunner::testSkipTwo test/test_pipeline.py::TestRunner::testSleep test/test_pipeline.py::TestRunner::testSleepNotCalledByDefault test/test_pipeline.py::TestRunner::testSleepNotCalledWhenZero test/test_pipeline.py::TestRunner::testSpecificationIsStored test/test_pipeline.py::TestRunner::testStepStdout test/test_pipeline.py::TestRunner::testStepWithoutName test/test_pipeline.py::TestRunner::testStepWithoutScript test/test_pipeline.py::TestRunner::testStepsDict test/test_pipeline.py::TestRunner::testStepsMustBeAList test/test_pipeline.py::TestRunner::testTaskScheduleTime test/test_pipeline.py::TestRunner::testTasksFollowingSchedule
setup.py slurm_pipeline/pipeline.py README.md
setup.py slurm_pipeline/pipeline.py README.md