Repository: Quantum-Accelerators/quacc
Pull request #1359 (2023-12-13 20:22:11)

Use partial functions in flows to increase user flexibility in setting job attributes (e.g. executor)
Continuation of #1322.

Challenges:
- Mainly Dask, which does not play nicely when `functools.partial()` is applied to a `Delayed` object. See https://github.com/dask/dask/issues/10707. There is now a workaround.
- Also there is a minor issue with dynamic switching of Covalent executors, but there is a workaround. See https://github.com/AgnostiqHQ/covalent/issues/1889

Remaining issue:

Need to be able to parallelize the following code on Dask.

```python
from dask.distributed import Client
from ase.build import bulk
from quacc.recipes.emt.slabs import bulk_to_slabs_flow
from functools import partial

client = Client()

atoms = bulk("Cu")
delayed = bulk_to_slabs_flow(atoms)
result = client.gather(client.compute(delayed))
```

Hint: To monitor, set `"logfile": ""`.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1359?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`395888b`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/commit/395888b4d36e10a151b9975cbd7fb2832111deb8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 98.90% compared to head [(`b435704`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1359?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 98.90%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1359      +/-   ##
==========================================
- Coverage   98.90%   98.90%   -0.01%     
==========================================
  Files          76       76              
  Lines        2932     2915      -17     
==========================================
- Hits         2900     2883      -17     
  Misses         32       32              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1359?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 669eddfd..1d0a8888 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -4,7 +4,8 @@
 
 - [ ] I have read the [Contributing Guide](https://quantum-accelerators.github.io/quacc/dev/contributing.html). Don't lie! 😉
 - [ ] My PR is on a custom branch and is _not_ named `main`.
-- [ ] I have added relevant unit tests.
+- [ ] I have used `black`, `isort`, and `ruff` as described in the [style guide](https://quantum-accelerators.github.io/quacc/dev/contributing.html#style).
+- [ ] I have added relevant, comprehensive [unit tests](https://quantum-accelerators.github.io/quacc/dev/contributing.html#unit-tests).
 
 ### Notes
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c57522c..8b678925 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,10 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ### Added
 
-- The `WORKFLOW_ENGINE` quacc setting now accepts `None`
+- The `WORKFLOW_ENGINE` quacc setting now accepts `None`.
+- A `DEBUG` quacc setting as been added.
 
 ### Changed
 
+- The way to run complex, dynamic flows has been modified to rely on `functools.partial()` instead of kwargs. See the updated documentation.
 - Refactored test suite
 
 ## [0.4.5]
diff --git a/docs/user/basics/wflow_syntax.md b/docs/user/basics/wflow_syntax.md
index 10c79997..035b7860 100644
--- a/docs/user/basics/wflow_syntax.md
+++ b/docs/user/basics/wflow_syntax.md
@@ -43,7 +43,7 @@ To help enable interoperability between workflow engines, quacc offers a unified
     <center>
 
     | Quacc               | Covalent                          |
-    | ------------------- | ---------------------------------|
+    | ------------------- | --------------------------------- |
     | `#!Python @job`     | `#!Python @delayed`               |
     | `#!Python @flow`    | No effect                         |
     | `#!Python @subflow` | `#!Python delayed(...).compute()` |
diff --git a/docs/user/recipes/recipes_intro.md b/docs/user/recipes/recipes_intro.md
index 89e9f7ea..f0f1600b 100644
--- a/docs/user/recipes/recipes_intro.md
+++ b/docs/user/recipes/recipes_intro.md
@@ -157,7 +157,7 @@ print(result)
      'volume': 11.761470249999999}
     ```
 
-### A Simple Mixed-Code Workflow
+### A Mixed-Code Workflow
 
 ```mermaid
 graph LR
@@ -272,6 +272,47 @@ print(result2)
     'volume': 11.761470249999999}
     ```
 
+### A More Complex Workflow
+
+```mermaid
+graph LR
+  A[Input] --> B(Make Slabs)
+  B --> C(Slab Relax) --> G(Slab Static) --> K[Output]
+  B --> D(Slab Relax) --> H(Slab Static) --> K[Output]
+  B --> E(Slab Relax) --> I(Slab Static) --> K[Output]
+  B --> F(Slab Relax) --> J(Slab Static) --> K[Output];
+```
+
+In this example, we will run a pre-made workflow that generates a set of slabs from a bulk structure and then runs a structure relaxation and static calculation on each slab. We will specifically highlight an example where we want to override the default parameters of one step in the recipe, in this case to tighten the force tolerance for the slab relaxation.
+
+!!! Tip
+
+    Unsure what arguments a given function takes? Check out the [API documentation](https://quantum-accelerators.github.io/quacc/reference/quacc/recipes/emt/slabs.html).
+
+```python
+from functools import partial
+from ase.build import bulk
+from quacc.recipes.emt.core import relax_job
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow
+
+# Define the Atoms object
+atoms = bulk("Cu")
+
+# Define the workflow
+custom_relax_job = partial(relax_job, opt_params={"fmax": 1e-4})  # (1)!
+result = bulk_to_slabs_flow(atoms, custom_relax_job=custom_relax_job)
+
+# Print the result
+print(result)
+```
+
+1. We have used a [partial function](https://www.learnpython.org/en/Partial_functions) here, which is a way to create a new function with specific arguments already applied. In other words, `#!Python opt_params={"fmax": 1e-4}` will be set as a keyword argument in the `relax_job` function by default. The same could be achieved, albeit more verbosely, as follows:
+
+   ```python
+   def custom_relax_job(*args, **kwargs):
+       return relax_job(*args, opt_params={"fmax": 1e-4}, **kwargs)
+   ```
+
 ## Concluding Comments
 
 At this point, you now have the basic idea of how quacc recipes work!
diff --git a/src/quacc/__init__.py b/src/quacc/__init__.py
index 13176049..2d6f38f7 100644
--- a/src/quacc/__init__.py
+++ b/src/quacc/__init__.py
@@ -5,7 +5,6 @@ from importlib.metadata import version
 from typing import TYPE_CHECKING
 
 from ase.atoms import Atoms
-from ase.io.jsonio import decode, encode
 
 from quacc.settings import QuaccSettings
 from quacc.wflow_tools.decorators import Flow, Job, Subflow, flow, job, subflow
@@ -17,6 +16,8 @@ __all__ = ["flow", "job", "subflow", "Flow", "Job", "Subflow"]
 
 
 def atoms_as_dict(s: Atoms) -> dict[str, Any]:
+    from ase.io.jsonio import encode
+
     # Uses Monty's MSONable spec
     # Normally, we would want to this to be a wrapper around atoms.todict() with @module and
     # @class key-value pairs inserted. However, atoms.todict()/atoms.fromdict() does not currently
@@ -25,6 +26,8 @@ def atoms_as_dict(s: Atoms) -> dict[str, Any]:
 
 
 def atoms_from_dict(d: dict[str, Any]) -> Atoms:
+    from ase.io.jsonio import decode
+
     # Uses Monty's MSONable spec
     # Normally, we would want to have this be a wrapper around atoms.fromdict()
     # that just ignores the @module/@class key-value pairs. However, atoms.todict()/atoms.fromdict()
@@ -41,3 +44,9 @@ Atoms.from_dict = atoms_from_dict
 
 # Load the settings
 SETTINGS = QuaccSettings()
+
+if SETTINGS.DEBUG:
+    import logging
+
+    logger = logging.getLogger(__name__)
+    logger.setLevel(logging.DEBUG)
diff --git a/src/quacc/recipes/common/defects.py b/src/quacc/recipes/common/defects.py
index d28bcd26..d67e40e7 100644
--- a/src/quacc/recipes/common/defects.py
+++ b/src/quacc/recipes/common/defects.py
@@ -7,7 +7,7 @@ from quacc import subflow
 from quacc.atoms.defects import make_defects_from_bulk
 
 if TYPE_CHECKING:
-    from typing import Callable
+    from typing import Any
 
     from ase.atoms import Atoms
 
@@ -19,7 +19,7 @@ def bulk_to_defects_subflow(
     atoms: Atoms,
     relax_job: Job,
     static_job: Job | None = None,
-    make_defects_fn: Callable = make_defects_from_bulk,
+    make_defects_kwargs: dict[str, Any] | None = None,
 ) -> list[dict]:
     """
     Workflow consisting of:
@@ -38,8 +38,8 @@ def bulk_to_defects_subflow(
         The relaxation function.
     static_job
         The static function.
-    make_defects_fn
-        The function for generating defects.
+    make_defects_kwargs
+        Keyword arguments for [quacc.atoms.defects.make_defects_from_bulk][]
 
     Returns
     -------
@@ -47,13 +47,13 @@ def bulk_to_defects_subflow(
         List of dictionary of results
     """
 
-    defects = make_defects_fn(atoms)
+    defects = make_defects_from_bulk(atoms, **make_defects_kwargs)
 
     results = []
     for defect in defects:
         result = relax_job(defect)
 
-        if static_job:
+        if static_job is not None:
             result = static_job(result["atoms"])
 
         results.append(result)
diff --git a/src/quacc/recipes/common/slabs.py b/src/quacc/recipes/common/slabs.py
index 88af5d67..812c142b 100644
--- a/src/quacc/recipes/common/slabs.py
+++ b/src/quacc/recipes/common/slabs.py
@@ -7,7 +7,7 @@ from quacc import subflow
 from quacc.atoms.slabs import make_adsorbate_structures, make_slabs_from_bulk
 
 if TYPE_CHECKING:
-    from typing import Callable
+    from typing import Any
 
     from ase.atoms import Atoms
 
@@ -19,7 +19,7 @@ def bulk_to_slabs_subflow(
     atoms: Atoms,
     relax_job: Job,
     static_job: Job | None = None,
-    make_slabs_fn: Callable = make_slabs_from_bulk,
+    make_slabs_kwargs: dict[str, Any] | None = None,
 ) -> list[dict]:
     """
     Workflow consisting of:
@@ -38,16 +38,18 @@ def bulk_to_slabs_subflow(
         The relaxation function.
     static_job
         The static function.
-    make_slabs_fn
-        The function for generating slabs.
+    make_slabs_kwargs
+        Additional keyword arguments to pass to
+        [quacc.atoms.slabs.make_slabs_from_bulk][]
 
     Returns
     -------
     list[dict]
         List of schemas.
     """
+    make_slabs_kwargs = make_slabs_kwargs or {}
 
-    slabs = make_slabs_fn(atoms)
+    slabs = make_slabs_from_bulk(atoms, **make_slabs_kwargs)
 
     results = []
     for slab in slabs:
@@ -67,7 +69,7 @@ def slab_to_ads_subflow(
     adsorbate: Atoms,
     relax_job: Job,
     static_job: Job | None,
-    make_ads_fn: Callable = make_adsorbate_structures,
+    make_ads_kwargs: dict[str, Any] | None = None,
 ) -> list[dict]:
     """
     Workflow consisting of:
@@ -88,22 +90,24 @@ def slab_to_ads_subflow(
         The slab releaxation job.
     static_job
         The slab static job.
-    make_ads_fn
-        The function to generate slab-adsorbate structures.
+    make_ads_kwargs
+        Additional keyword arguments to pass to
+        [quacc.atoms.slabs.make_adsorbate_structures][]
 
     Returns
     -------
     list[dict]
         List of schemas.
     """
+    make_ads_kwargs = make_ads_kwargs or {}
 
-    slabs = make_ads_fn(atoms, adsorbate)
+    slabs = make_adsorbate_structures(atoms, adsorbate, **make_ads_kwargs)
 
     results = []
     for slab in slabs:
         result = relax_job(slab)
 
-        if static_job:
+        if static_job is not None:
             result = static_job(result["atoms"])
 
         results.append(result)
diff --git a/src/quacc/recipes/emt/defects.py b/src/quacc/recipes/emt/defects.py
index e5cb5e81..04312c99 100644
--- a/src/quacc/recipes/emt/defects.py
+++ b/src/quacc/recipes/emt/defects.py
@@ -1,15 +1,14 @@
 """Defect recipes for EMT."""
 from __future__ import annotations
 
-from functools import partial
 from typing import TYPE_CHECKING
 
 from pymatgen.analysis.defects.generators import VacancyGenerator
 
 from quacc import flow
-from quacc.atoms.defects import make_defects_from_bulk
 from quacc.recipes.common.defects import bulk_to_defects_subflow
 from quacc.recipes.emt.core import relax_job, static_job
+from quacc.utils.dicts import merge_dicts
 
 if TYPE_CHECKING:
     from typing import Any
@@ -23,6 +22,7 @@ if TYPE_CHECKING:
         VoronoiInterstitialGenerator,
     )
 
+    from quacc import Job
     from quacc.schemas._aliases.ase import OptSchema, RunSchema
 
 
@@ -38,10 +38,10 @@ def bulk_to_defects_flow(
         | VoronoiInterstitialGenerator
     ) = VacancyGenerator,
     defect_charge: int = 0,
-    make_defects_kwargs: dict[str, Any] | None = None,
+    custom_relax_job: Job | None = None,
+    custom_static_job: Job | None = None,
     run_static: bool = True,
-    defect_relax_kwargs: dict[str, Any] | None = None,
-    defect_static_kwargs: dict[str, Any] | None = None,
+    make_defects_kwargs: dict[str, Any] | None = None,
 ) -> list[RunSchema | OptSchema]:
     """
     Workflow consisting of:
@@ -60,15 +60,13 @@ def bulk_to_defects_flow(
         Defect generator
     defect_charge
         Charge state of the defect
+    custom_relax_job
+        Relaxation job, which defaults to [quacc.recipes.emt.core.relax_job][].
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.emt.core.static_job][].
     make_defects_kwargs
         Keyword arguments to pass to
         [quacc.atoms.defects.make_defects_from_bulk][]
-    run_static
-        Whether to run the static calculation.
-    defect_relax_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.emt.core.relax_job][].
-    defect_static_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.emt.core.static_job][].
 
     Returns
     -------
@@ -76,18 +74,15 @@ def bulk_to_defects_flow(
         List of dictionary of results from [quacc.schemas.ase.summarize_run][]
         or [quacc.schemas.ase.summarize_opt_run][]
     """
-    make_defects_kwargs = make_defects_kwargs or {}
-    defect_relax_kwargs = defect_relax_kwargs or {}
-    defect_static_kwargs = defect_static_kwargs or {}
+    make_defects_kwargs = merge_dicts(
+        make_defects_kwargs, {"defect_gen": defect_gen, "defect_charge": defect_charge}
+    )
 
     return bulk_to_defects_subflow(
         atoms,
-        partial(relax_job, **defect_relax_kwargs),
-        static_job=partial(static_job, **defect_static_kwargs) if run_static else None,
-        make_defects_fn=partial(
-            make_defects_from_bulk,
-            defect_gen=defect_gen,
-            defect_charge=defect_charge,
-            **make_defects_kwargs,
-        ),
+        relax_job if custom_relax_job is None else custom_relax_job,
+        static_job=(static_job if custom_static_job is None else custom_static_job)
+        if run_static
+        else None,
+        make_defects_kwargs=make_defects_kwargs,
     )
diff --git a/src/quacc/recipes/emt/phonons.py b/src/quacc/recipes/emt/phonons.py
index e331a8b4..5fcc7120 100644
--- a/src/quacc/recipes/emt/phonons.py
+++ b/src/quacc/recipes/emt/phonons.py
@@ -1,7 +1,6 @@
 """Phonon recipes for EMT"""
 from __future__ import annotations
 
-from functools import partial
 from typing import TYPE_CHECKING
 
 from quacc import flow
@@ -9,11 +8,10 @@ from quacc.recipes.common.phonons import phonon_flow as phonon_flow_
 from quacc.recipes.emt.core import static_job
 
 if TYPE_CHECKING:
-    from typing import Any
-
     from ase.atoms import Atoms
     from numpy.typing import ArrayLike
 
+    from quacc import Job
     from quacc.schemas._aliases.phonons import PhononSchema
 
 
@@ -26,7 +24,7 @@ def phonon_flow(
     t_step: float = 10,
     t_min: float = 0,
     t_max: float = 1000,
-    static_job_kwargs: dict[str, Any] | None = None,
+    custom_static_job: Job | None = None,
 ) -> PhononSchema:
     """
     Carry out a phonon calculation.
@@ -47,20 +45,19 @@ def phonon_flow(
         Min temperature (K).
     t_max
         Max temperature (K).
-    static_job_kwargs
-        Additional keyword arguments for [quacc.recipes.emt.core.static_job][]
-        for the force calculations.
+    static_job
+        The static job for the force calculations, which defaults
+        to [quacc.recipes.emt.core.static_job][]
 
     Returns
     -------
     PhononSchema
         Dictionary of results from [quacc.schemas.phonons.summarize_phonopy][]
     """
-    static_job_kwargs = static_job_kwargs or {}
 
     return phonon_flow_(
         atoms,
-        partial(static_job, **static_job_kwargs),
+        static_job if custom_static_job is None else custom_static_job,
         supercell_matrix=supercell_matrix,
         atom_disp=atom_disp,
         t_step=t_step,
diff --git a/src/quacc/recipes/emt/slabs.py b/src/quacc/recipes/emt/slabs.py
index c789639a..cb048a93 100644
--- a/src/quacc/recipes/emt/slabs.py
+++ b/src/quacc/recipes/emt/slabs.py
@@ -1,11 +1,9 @@
 """Slab recipes for EMT."""
 from __future__ import annotations
 
-from functools import partial
 from typing import TYPE_CHECKING
 
 from quacc import flow
-from quacc.atoms.slabs import make_slabs_from_bulk
 from quacc.recipes.common.slabs import bulk_to_slabs_subflow
 from quacc.recipes.emt.core import relax_job, static_job
 
@@ -14,16 +12,17 @@ if TYPE_CHECKING:
 
     from ase.atoms import Atoms
 
+    from quacc import Job
     from quacc.schemas._aliases.ase import OptSchema, RunSchema
 
 
 @flow
 def bulk_to_slabs_flow(
     atoms: Atoms,
-    make_slabs_kwargs: dict[str, Any] | None = None,
+    custom_relax_job: Job | None = None,
+    custom_static_job: Job | None = None,
     run_static: bool = True,
-    slab_relax_kwargs: dict[str, Any] | None = None,
-    slab_static_kwargs: dict[str, Any] | None = None,
+    make_slabs_kwargs: dict[str, Any] | None = None,
 ) -> list[RunSchema | OptSchema]:
     """
     Workflow consisting of:
@@ -38,15 +37,15 @@ def bulk_to_slabs_flow(
     ----------
     atoms
         Atoms object
+    custom_relax_job
+        The relaxation job, which defaults to [quacc.recipes.emt.core.relax_job][].
+    custom_static_job
+        The static job, which defaults to [quacc.recipes.emt.core.static_job][].
+    run_static
+        Whether to run static calculations.
     make_slabs_kwargs
         Additional keyword arguments to pass to
         [quacc.atoms.slabs.make_slabs_from_bulk][]
-    run_static
-        Whether to run the static calculation.
-    slab_relax_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.emt.core.relax_job][].
-    slab_static_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.emt.core.static_job][].
 
     Returns
     -------
@@ -55,13 +54,11 @@ def bulk_to_slabs_flow(
         [OptSchema][quacc.schemas.ase.summarize_opt_run] for each slab.
     """
 
-    make_slabs_kwargs = make_slabs_kwargs or {}
-    slab_relax_kwargs = slab_relax_kwargs or {}
-    slab_static_kwargs = slab_static_kwargs or {}
-
     return bulk_to_slabs_subflow(
         atoms,
-        partial(relax_job, **slab_relax_kwargs),
-        static_job=partial(static_job, **slab_static_kwargs) if run_static else None,
-        make_slabs_fn=partial(make_slabs_from_bulk, **make_slabs_kwargs),
+        relax_job if custom_relax_job is None else custom_relax_job,
+        static_job=(static_job if custom_static_job is None else custom_relax_job)
+        if run_static
+        else None,
+        make_slabs_kwargs=make_slabs_kwargs,
     )
diff --git a/src/quacc/recipes/tblite/phonons.py b/src/quacc/recipes/tblite/phonons.py
index 1f183190..b64b1a7d 100644
--- a/src/quacc/recipes/tblite/phonons.py
+++ b/src/quacc/recipes/tblite/phonons.py
@@ -1,19 +1,17 @@
 """Phonon recipes for TBLite"""
 from __future__ import annotations
 
-from functools import partial
 from typing import TYPE_CHECKING
 
 from quacc import flow
-from quacc.recipes.common.phonons import phonon_flow as phonon_flow_
+from quacc.recipes.common.phonons import phonon_flow as common_phonon_flow
 from quacc.recipes.tblite.core import static_job
 
 if TYPE_CHECKING:
-    from typing import Any
-
     from ase.atoms import Atoms
     from numpy.typing import ArrayLike
 
+    from quacc import Job
     from quacc.schemas._aliases.phonons import PhononSchema
 
 
@@ -26,7 +24,7 @@ def phonon_flow(
     t_step: float = 10,
     t_min: float = 0,
     t_max: float = 1000,
-    static_job_kwargs: dict[str, Any] | None = None,
+    custom_static_job: Job | None = None,
 ) -> PhononSchema:
     """
     Carry out a phonon calculation.
@@ -47,20 +45,18 @@ def phonon_flow(
         Min temperature (K).
     t_max
         Max temperature (K).
-    static_job_kwargs
-        Keyword arguments for [quacc.recipes.tblite.core.static_job][]
-        for the force calculations.
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.tblite.core.static_job][]
 
     Returns
     -------
     PhononSchema
         Dictionary of results from [quacc.schemas.phonons.summarize_phonopy][]
     """
-    static_job_kwargs = static_job_kwargs or {}
 
-    return phonon_flow_(
+    return common_phonon_flow(
         atoms,
-        partial(static_job, **static_job_kwargs),
+        static_job if custom_static_job is None else custom_static_job,
         supercell_matrix=supercell_matrix,
         atom_disp=atom_disp,
         t_step=t_step,
diff --git a/src/quacc/recipes/vasp/mp.py b/src/quacc/recipes/vasp/mp.py
index a21dacf7..9b66386b 100644
--- a/src/quacc/recipes/vasp/mp.py
+++ b/src/quacc/recipes/vasp/mp.py
@@ -24,10 +24,9 @@ from quacc import flow, job
 from quacc.recipes.vasp._base import base_fn
 
 if TYPE_CHECKING:
-    from typing import Any
-
     from ase.atoms import Atoms
 
+    from quacc import Job
     from quacc.schemas._aliases.vasp import MPRelaxFlowSchema, VaspSchema
 
 
@@ -127,9 +126,7 @@ def mp_relax_job(
 
 @flow
 def mp_relax_flow(
-    atoms: Atoms,
-    prerelax_job_kwargs: dict[str, Any] | None = None,
-    relax_job_kwargs: dict[str, Any] | None = None,
+    atoms: Atoms, prerelax_job: Job = mp_prerelax_job, relax_job: Job = mp_relax_job
 ) -> MPRelaxFlowSchema:
     """
     Workflow consisting of:
@@ -142,31 +139,28 @@ def mp_relax_flow(
     ----------
     atoms
         Atoms object for the structure.
-    prerelax_job_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.mp.mp_prerelax_job][].
-    relax_job_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.mp.mp_relax_job][].
+    prerelax_job
+        Pre-relaxation job, which defaults to [quacc.recipes.vasp.mp.mp_prerelax_job][].
+    relax_job
+        Relaxation job, which defaults to [quacc.recipes.vasp.mp.mp_relax_job][].
 
     Returns
     -------
     MPRelaxFlowSchema
         Dictionary of results
     """
-    prerelax_job_kwargs = prerelax_job_kwargs or {}
-    relax_job_kwargs = relax_job_kwargs or {}
 
     # Run the prerelax
-    prerelax_results = mp_prerelax_job(atoms, **prerelax_job_kwargs)
+    prerelax_results = prerelax_job(atoms)
 
     # Run the relax
-    relax_results = mp_relax_job(
+    relax_results = relax_job(
         prerelax_results["atoms"],
         bandgap=prerelax_results["output"]["bandgap"],
         copy_files=[
             Path(prerelax_results["dir_name"]) / "CHGCAR",
             Path(prerelax_results["dir_name"]) / "WAVECAR",
         ],
-        **relax_job_kwargs,
     )
     relax_results["prerelax"] = prerelax_results
 
diff --git a/src/quacc/recipes/vasp/slabs.py b/src/quacc/recipes/vasp/slabs.py
index 47ea25a6..5c2a3cb6 100644
--- a/src/quacc/recipes/vasp/slabs.py
+++ b/src/quacc/recipes/vasp/slabs.py
@@ -1,11 +1,9 @@
 """Recipes for slabs."""
 from __future__ import annotations
 
-from functools import partial
 from typing import TYPE_CHECKING
 
 from quacc import flow, job
-from quacc.atoms.slabs import make_adsorbate_structures, make_slabs_from_bulk
 from quacc.recipes.common.slabs import bulk_to_slabs_subflow, slab_to_ads_subflow
 from quacc.recipes.vasp._base import base_fn
 
@@ -15,11 +13,12 @@ if TYPE_CHECKING:
 
     from ase.atoms import Atoms
 
+    from quacc import Job
     from quacc.schemas._aliases.vasp import VaspSchema
 
 
 @job
-def slab_static_job(
+def static_job(
     atoms: Atoms,
     preset: str | None = "SlabSet",
     copy_files: str | Path | list[str | Path] | None = None,
@@ -69,7 +68,7 @@ def slab_static_job(
 
 
 @job
-def slab_relax_job(
+def relax_job(
     atoms: Atoms,
     preset: str | None = "SlabSet",
     copy_files: str | Path | list[str | Path] | None = None,
@@ -122,9 +121,9 @@ def slab_relax_job(
 def bulk_to_slabs_flow(
     atoms: Atoms,
     make_slabs_kwargs: dict[str, Any] | None = None,
+    custom_relax_job: Job | None = None,
+    custom_static_job: Job | None = None,
     run_static: bool = True,
-    slab_relax_kwargs: dict[str, Any] | None = None,
-    slab_static_kwargs: dict[str, Any] | None = None,
 ) -> list[VaspSchema]:
     """
     Workflow consisting of:
@@ -141,12 +140,12 @@ def bulk_to_slabs_flow(
         Atoms object
     make_slabs_kwargs
         Additional keyword arguments to pass to [quacc.atoms.slabs.make_slabs_from_bulk][]
+    custom_relax_job
+        Relaxation job, which defaults to [quacc.recipes.vasp.slabs.slab_relax_job][].
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.vasp.slabs.slab_static_job][].
     run_static
-        Whether to run the static calculation.
-    slab_relax_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_relax_job][].
-    slab_static_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_static_job][].
+        Whether to run static calculations.
 
     Returns
     -------
@@ -154,19 +153,13 @@ def bulk_to_slabs_flow(
         List of dictionary results from [quacc.schemas.vasp.vasp_summarize_run][]
     """
 
-    make_slabs_kwargs = make_slabs_kwargs or {}
-    slab_relax_kwargs = slab_relax_kwargs or {}
-    slab_static_kwargs = slab_static_kwargs or {}
-
-    relax_job = partial(slab_relax_job, **slab_relax_kwargs)
-    static_job = partial(slab_static_job, **slab_static_kwargs)
-    make_slabs_fn = partial(make_slabs_from_bulk, **make_slabs_kwargs)
-
     return bulk_to_slabs_subflow(
         atoms,
-        relax_job,
-        static_job=static_job if run_static else None,
-        make_slabs_fn=make_slabs_fn,
+        relax_job if custom_relax_job is None else custom_relax_job,
+        static_job=(static_job if custom_static_job is None else custom_static_job)
+        if run_static
+        else None,
+        make_slabs_kwargs=make_slabs_kwargs,
     )
 
 
@@ -174,10 +167,10 @@ def bulk_to_slabs_flow(
 def slab_to_ads_flow(
     slab: Atoms,
     adsorbate: Atoms,
-    make_ads_kwargs: dict[str, Any] | None = None,
+    custom_relax_job: Job | None = None,
+    custom_static_job: Job | None = None,
     run_static: bool = True,
-    slab_relax_kwargs: dict[str, Any] | None = None,
-    slab_static_kwargs: dict[str, Any] | None = None,
+    make_ads_kwargs: dict[str, Any] | None = None,
 ) -> list[VaspSchema]:
     """
     Workflow consisting of:
@@ -194,14 +187,14 @@ def slab_to_ads_flow(
         Atoms object for the slab structure.
     adsorbate
         Atoms object for the adsorbate.
+    custom_relax_job
+        Relaxation job, which defaults to [quacc.recipes.vasp.slabs.slab_relax_job][].
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.vasp.slabs.slab_static_job][].
+    run_static
+        Whether to run static calculations.
     make_ads_kwargs
         Additional keyword arguments to pass to [quacc.atoms.slabs.make_adsorbate_structures][]
-    run_static
-        Whether to run the static calculation.
-    slab_relax_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_relax_job][].
-    slab_static_kwargs
-        Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_static_job][].
 
     Returns
     -------
@@ -209,16 +202,12 @@ def slab_to_ads_flow(
         List of dictionaries of results from [quacc.schemas.vasp.vasp_summarize_run][]
     """
 
-    make_ads_kwargs = make_ads_kwargs or {}
-    slab_relax_kwargs = slab_relax_kwargs or {}
-    slab_static_kwargs = slab_static_kwargs or {}
-
     return slab_to_ads_subflow(
         slab,
         adsorbate,
-        partial(slab_relax_job, **slab_relax_kwargs),
-        static_job=partial(slab_static_job, **slab_static_kwargs)
+        relax_job if custom_relax_job is None else custom_relax_job,
+        static_job=(static_job if custom_static_job is None else custom_static_job)
         if run_static
         else None,
-        make_ads_fn=partial(make_adsorbate_structures, **make_ads_kwargs),
+        make_ads_kwargs=make_ads_kwargs,
     )
diff --git a/src/quacc/runners/ase.py b/src/quacc/runners/ase.py
index f885ee98..bc15ee69 100644
--- a/src/quacc/runners/ase.py
+++ b/src/quacc/runners/ase.py
@@ -1,6 +1,7 @@
 """Utility functions for running ASE calculators with ASE-based methods."""
 from __future__ import annotations
 
+import sys
 from typing import TYPE_CHECKING
 
 import numpy as np
@@ -11,6 +12,7 @@ from ase.vibrations import Vibrations
 from monty.dev import requires
 from monty.os.path import zpath
 
+from quacc import SETTINGS
 from quacc.atoms.core import copy_atoms
 from quacc.runners.prep import calc_cleanup, calc_setup
 from quacc.utils.dicts import merge_dicts
@@ -159,7 +161,10 @@ def run_opt(
 
     # Set defaults
     optimizer_kwargs = merge_dicts(
-        {"logfile": tmpdir / "opt.log", "restart": tmpdir / "opt.pckl"},
+        {
+            "logfile": "-" if SETTINGS.DEBUG else tmpdir / "opt.log",
+            "restart": tmpdir / "opt.pckl",
+        },
         optimizer_kwargs,
     )
     run_kwargs = run_kwargs or {}
@@ -240,7 +245,7 @@ def run_vib(
     vib.run()
 
     # Summarize run
-    vib.summary(log=str(tmpdir / "vib_summary.log"))
+    vib.summary(log=sys.stdout if SETTINGS.DEBUG else str(tmpdir / "vib_summary.log"))
 
     # Perform cleanup operations
     calc_cleanup(tmpdir, job_results_dir)
diff --git a/src/quacc/runners/prep.py b/src/quacc/runners/prep.py
index 7532f05e..95bb60f6 100644
--- a/src/quacc/runners/prep.py
+++ b/src/quacc/runners/prep.py
@@ -104,7 +104,7 @@ def calc_cleanup(tmpdir: Path, job_results_dir: Path) -> None:
 
     # Move files from tmpdir to job_results_dir
     for file_name in os.listdir(tmpdir):
-        if Path(job_results_dir / file_name).exists():
+        if Path(job_results_dir / file_name).is_dir():
             remove(job_results_dir / file_name)
         move(tmpdir / file_name, job_results_dir / file_name)
 
diff --git a/src/quacc/schemas/ase.py b/src/quacc/schemas/ase.py
index f37d5fe4..6490ce41 100644
--- a/src/quacc/schemas/ase.py
+++ b/src/quacc/schemas/ase.py
@@ -388,9 +388,13 @@ def summarize_ideal_gas_thermo(
     results = {
         "results": {
             "energy": igt.potentialenergy,
-            "enthalpy": igt.get_enthalpy(temperature),
-            "entropy": igt.get_entropy(temperature, pressure * 10**5),
-            "gibbs_energy": igt.get_gibbs_energy(temperature, pressure * 10**5),
+            "enthalpy": igt.get_enthalpy(temperature, verbose=SETTINGS.DEBUG),
+            "entropy": igt.get_entropy(
+                temperature, pressure * 10**5, verbose=SETTINGS.DEBUG
+            ),
+            "gibbs_energy": igt.get_gibbs_energy(
+                temperature, pressure * 10**5, verbose=SETTINGS.DEBUG
+            ),
             "zpe": igt.get_ZPE_correction(),
         }
     }
diff --git a/src/quacc/settings.py b/src/quacc/settings.py
index 62bae999..bcec15b2 100644
--- a/src/quacc/settings.py
+++ b/src/quacc/settings.py
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Literal, Optional, Union
 
 import psutil
 from maggma.core import Store
-from monty.json import MontyDecoder
 from pydantic import Field, field_validator, model_validator
 from pydantic_settings import BaseSettings, SettingsConfigDict
 
@@ -323,15 +322,24 @@ class QuaccSettings(BaseSettings):
         "config.yml", description="Path to NewtonNet YAML settings file"
     )
 
+    # ---------------------------
+    # Debug Settings
+    # ---------------------------
+    DEBUG: bool = Field(
+        False,
+        description=(
+            "Whether to run in debug mode. This will set the logging level to DEBUG, "
+            "ASE logs (e.g. optimizations, vibrations, thermo) are printed to stdout."
+        ),
+    )
+
     # --8<-- [end:settings]
 
     @field_validator("WORKFLOW_ENGINE")
     @classmethod
     def validate_workflow_engine(cls, v: Optional[str]) -> Optional[str]:
         """Validate the workflow engine"""
-        if v and v.lower() == "local":
-            return None
-        return v
+        return None if v and v.lower() == "local" else v
 
     @field_validator("RESULTS_DIR", "SCRATCH_DIR")
     @classmethod
@@ -366,6 +374,8 @@ class QuaccSettings(BaseSettings):
     @field_validator("PRIMARY_STORE")
     def generate_store(cls, v: Union[str, Store]) -> Store:
         """Generate the Maggma store"""
+        from monty.json import MontyDecoder
+
         return MontyDecoder().decode(v) if isinstance(v, str) else v
 
     model_config = SettingsConfigDict(env_prefix="quacc_")


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e3ddbc20..636e8e9c 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -384,7 +384,7 @@ jobs:
         uses: Wandalen/wretry.action@v1
         with:
           action: codecov/codecov-action@v3
-          attempt_limit: 5
+          attempt_limit: 10
           attempt_delay: 30000
           with: |
             fail_ci_if_error: true
diff --git a/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py b/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py
index 1bb2f1e8..68777006 100644
--- a/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py
+++ b/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py
@@ -1,8 +1,11 @@
+from functools import partial
+
 import pytest
 from ase.build import bulk
 
 pytest.importorskip("pymatgen.analysis.defects")
 
+from quacc.recipes.emt.core import relax_job
 from quacc.recipes.emt.defects import bulk_to_defects_flow
 
 
@@ -10,14 +13,16 @@ def test_bulk_to_defects_flow(tmp_path, monkeypatch):
     monkeypatch.chdir(tmp_path)
     atoms = bulk("Cu")
     output = bulk_to_defects_flow(
-        atoms, defect_relax_kwargs={"opt_params": {"fmax": 5}}
+        atoms, custom_relax_job=partial(relax_job, opt_params={"fmax": 5})
     )
     assert len(output) == 1
     assert len(output[0]["atoms"]) == 107
 
     atoms = bulk("Cu")
     output = bulk_to_defects_flow(
-        atoms, run_static=False, defect_relax_kwargs={"opt_params": {"fmax": 5}}
+        atoms,
+        custom_relax_job=partial(relax_job, opt_params={"fmax": 5}),
+        run_static=False,
     )
     assert len(output) == 1
     assert len(output[0]["atoms"]) == 107
diff --git a/tests/core/recipes/emt_recipes/test_emt_recipes.py b/tests/core/recipes/emt_recipes/test_emt_recipes.py
index f832a457..11927525 100644
--- a/tests/core/recipes/emt_recipes/test_emt_recipes.py
+++ b/tests/core/recipes/emt_recipes/test_emt_recipes.py
@@ -1,3 +1,5 @@
+from functools import partial
+
 import numpy as np
 import pytest
 from ase.build import bulk, molecule
@@ -94,7 +96,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch):
     outputs = bulk_to_slabs_flow(
         atoms,
         run_static=False,
-        slab_relax_kwargs={"opt_params": {"fmax": 1.0}, "asap_cutoff": True},
+        custom_relax_job=partial(relax_job, opt_params={"fmax": 1.0}, asap_cutoff=True),
     )
     assert len(outputs) == 4
     assert outputs[0]["nsites"] == 80
diff --git a/tests/core/recipes/tblite_recipes/test_tblite_phonons.py b/tests/core/recipes/tblite_recipes/test_tblite_phonons.py
index 551971da..0e5515e9 100644
--- a/tests/core/recipes/tblite_recipes/test_tblite_phonons.py
+++ b/tests/core/recipes/tblite_recipes/test_tblite_phonons.py
@@ -1,6 +1,9 @@
+from functools import partial
+
 import pytest
 from ase.build import bulk
 
+from quacc.recipes.tblite.core import static_job as static_job_
 from quacc.recipes.tblite.phonons import phonon_flow
 
 pytest.importorskip("tblite.ase")
@@ -10,6 +13,8 @@ pytest.importorskip("phonopy")
 def test_phonon_flow(tmp_path, monkeypatch):
     monkeypatch.chdir(tmp_path)
     atoms = bulk("Cu")
-    output = phonon_flow(atoms, static_job_kwargs={"method": "GFN1-xTB"})
+    output = phonon_flow(
+        atoms, custom_static_job=partial(static_job_, method="GFN1-xTB")
+    )
     assert output["results"]["force_constants"].shape == (8, 8, 3, 3)
     assert len(output["results"]["thermal_properties"]["temperatures"]) == 101
diff --git a/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py b/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py
index 2ef4c984..0eaac567 100644
--- a/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py
+++ b/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py
@@ -1,3 +1,5 @@
+from functools import partial
+
 import pytest
 from ase.build import bulk, molecule
 
@@ -5,12 +7,10 @@ from quacc import SETTINGS
 from quacc.recipes.vasp.core import double_relax_job, relax_job, static_job
 from quacc.recipes.vasp.mp import mp_prerelax_job, mp_relax_flow, mp_relax_job
 from quacc.recipes.vasp.qmof import qmof_relax_job
-from quacc.recipes.vasp.slabs import (
-    bulk_to_slabs_flow,
-    slab_relax_job,
-    slab_static_job,
-    slab_to_ads_flow,
-)
+from quacc.recipes.vasp.slabs import bulk_to_slabs_flow
+from quacc.recipes.vasp.slabs import relax_job as slab_relax_job
+from quacc.recipes.vasp.slabs import slab_to_ads_flow
+from quacc.recipes.vasp.slabs import static_job as slab_static_job
 
 DEFAULT_SETTINGS = SETTINGS.model_copy()
 
@@ -218,7 +218,9 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch):
     assert [output["parameters"]["nsw"] == 0 for output in outputs]
 
     outputs = bulk_to_slabs_flow(
-        atoms, slab_relax_kwargs={"preset": "SlabSet", "nelmin": 6}, run_static=False
+        atoms,
+        custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6),
+        run_static=False,
     )
     assert len(outputs) == 4
     assert outputs[0]["nsites"] == 45
@@ -230,7 +232,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch):
     assert [output["parameters"]["encut"] == 450 for output in outputs]
 
     outputs = bulk_to_slabs_flow(
-        atoms, slab_static_kwargs={"preset": "SlabSet", "nelmin": 6}
+        atoms, custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6)
     )
     assert len(outputs) == 4
     assert outputs[0]["nsites"] == 45
@@ -257,7 +259,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch):
     outputs = slab_to_ads_flow(
         atoms,
         adsorbate,
-        slab_relax_kwargs={"preset": "SlabSet", "nelmin": 6},
+        custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6),
         run_static=False,
     )
 
@@ -267,7 +269,9 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch):
     assert [output["parameters"]["encut"] == 450 for output in outputs]
 
     outputs = slab_to_ads_flow(
-        atoms, adsorbate, slab_static_kwargs={"preset": "SlabSet", "nelmin": 6}
+        atoms,
+        adsorbate,
+        custom_static_job=partial(slab_static_job, preset="SlabSet", nelmin=6),
     )
 
     assert [output["nsites"] == 82 for output in outputs]
diff --git a/tests/core/schemas/test_cclib_schema.py b/tests/core/schemas/test_cclib_schema.py
index 9a15408a..f0fa4a75 100644
--- a/tests/core/schemas/test_cclib_schema.py
+++ b/tests/core/schemas/test_cclib_schema.py
@@ -105,7 +105,6 @@ def test_cclib_summarize_run(tmp_path, monkeypatch):
     MontyDecoder().process_decoded(d)
 
     # Make sure default dir works
-    cwd = os.getcwd()
     monkeypatch.chdir(run1)
     cclib_summarize_run(atoms, ".log")
 
diff --git a/tests/core/settings/test_settings.py b/tests/core/settings/test_settings.py
index 9a53613a..dd270be4 100644
--- a/tests/core/settings/test_settings.py
+++ b/tests/core/settings/test_settings.py
@@ -29,16 +29,17 @@ def teardown_function():
 def test_file_v1(tmp_path, monkeypatch):
     monkeypatch.chdir(tmp_path)
 
-    assert QuaccSettings().GZIP_FILES is True
+    assert QuaccSettings().DEBUG is False
 
     with open("quacc_test.yaml", "w") as f:
-        f.write("GZIP_FILES: false\nWORKFLOW_ENGINE: local")
+        f.write("GZIP_FILES: false\nWORKFLOW_ENGINE: local\nDEBUG: true")
     monkeypatch.setenv(
         "QUACC_CONFIG_FILE", os.path.join(os.getcwd(), "quacc_test.yaml")
     )
 
     assert QuaccSettings().GZIP_FILES is False
     assert QuaccSettings().WORKFLOW_ENGINE is None
+    assert QuaccSettings().DEBUG is True
     os.remove("quacc_test.yaml")
 
 
diff --git a/tests/covalent/.quacc.yaml b/tests/covalent/.quacc.yaml
index f8ed9bce..6e31f74b 100644
--- a/tests/covalent/.quacc.yaml
+++ b/tests/covalent/.quacc.yaml
@@ -1,3 +1,4 @@
+DEBUG: true
 RESULTS_DIR: ./.test_results
 SCRATCH_DIR: ./.test_scratch
 WORKFLOW_ENGINE: covalent
diff --git a/tests/covalent/test_covalent_recipes.py b/tests/covalent/test_covalent_recipes.py
index bfcb342e..efc2fb41 100644
--- a/tests/covalent/test_covalent_recipes.py
+++ b/tests/covalent/test_covalent_recipes.py
@@ -1,3 +1,5 @@
+from functools import partial
+
 import pytest
 from ase.build import bulk
 
@@ -10,6 +12,22 @@ pytestmark = pytest.mark.skipif(
 )
 
 from quacc.recipes.emt.core import relax_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
+
+
+def test_covalent_functools(tmp_path, monkeypatch):
+    monkeypatch.chdir(tmp_path)
+    atoms = bulk("Cu")
+    dispatch_id = ct.dispatch(bulk_to_slabs_flow)(
+        atoms,
+        custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}),
+        run_static=False,
+    )
+    output = ct.get_result(dispatch_id, wait=True)
+    assert output.status == "COMPLETED"
+    assert len(output.result) == 4
+    assert "atoms" in output.result[-1]
+    assert output.result[-1]["fmax"] == 0.1
 
 
 def test_phonon_flow(tmp_path, monkeypatch):
diff --git a/tests/dask/.quacc.yaml b/tests/dask/.quacc.yaml
index f4384162..64eca58c 100644
--- a/tests/dask/.quacc.yaml
+++ b/tests/dask/.quacc.yaml
@@ -1,4 +1,5 @@
 CREATE_UNIQUE_DIR: true
+DEBUG: true
 RESULTS_DIR: ./.test_results
 SCRATCH_DIR: ./.test_scratch
 WORKFLOW_ENGINE: dask
diff --git a/tests/dask/test_dask_recipes.py b/tests/dask/test_dask_recipes.py
index c3171680..a058ac97 100644
--- a/tests/dask/test_dask_recipes.py
+++ b/tests/dask/test_dask_recipes.py
@@ -1,3 +1,5 @@
+from functools import partial
+
 import pytest
 from ase.build import bulk
 
@@ -12,10 +14,29 @@ pytestmark = pytest.mark.skipif(
 from dask.distributed import default_client
 
 from quacc.recipes.emt.core import relax_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 client = default_client()
 
 
+def test_dask_functools(tmp_path, monkeypatch):
+    from dask import delayed as delayed_
+
+    monkeypatch.chdir(tmp_path)
+    atoms = bulk("Cu")
+    delayed = bulk_to_slabs_flow(
+        atoms,
+        custom_relax_job=delayed_(
+            partial(relax_job.__wrapped__, opt_params={"fmax": 0.1})
+        ),
+        run_static=False,
+    )
+    result = client.gather(client.compute(delayed))
+    assert len(result) == 4
+    assert "atoms" in result[-1]
+    assert result[-1]["fmax"] == 0.1
+
+
 def test_dask_phonon_flow(tmp_path, monkeypatch):
     pytest.importorskip("phonopy")
     from quacc.recipes.emt.phonons import phonon_flow
diff --git a/tests/dask/test_dask_tutorials.py b/tests/dask/test_dask_tutorials.py
index f6c0401c..1c0b4eaa 100644
--- a/tests/dask/test_dask_tutorials.py
+++ b/tests/dask/test_dask_tutorials.py
@@ -1,7 +1,7 @@
 import pytest
 from ase.build import bulk, molecule
 
-from quacc import SETTINGS, job, subflow
+from quacc import SETTINGS
 
 dask = pytest.importorskip("dask")
 pytestmark = pytest.mark.skipif(
@@ -109,13 +109,7 @@ def test_tutorial2c(tmp_path, monkeypatch):
     # Define the workflow
     def workflow(atoms):
         relaxed_bulk = relax_job(atoms)
-        return bulk_to_slabs_flow(
-            relaxed_bulk["atoms"],
-            run_static=False,
-            # slab_relax_kwargs={
-            #     "opt_params": {"optimizer_kwargs": {"logfile": "-"}}
-            # },  # this is for easy debugging)  # (1)!
-        )
+        return bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False)  # (1)!
 
     # Define the Atoms object
     atoms = bulk("Cu")
@@ -129,60 +123,3 @@ def test_tutorial2c(tmp_path, monkeypatch):
     # Print the results
     assert len(result) == 4
     assert "atoms" in result[0]
-
-
-def test_comparison1(tmp_path, monkeypatch):
-    monkeypatch.chdir(tmp_path)
-
-    @job  #  (1)!
-    def add(a, b):
-        return a + b
-
-    @job
-    def mult(a, b):
-        return a * b
-
-    def workflow(a, b, c):  #  (2)!
-        return mult(add(a, b), c)
-
-    result = client.compute(workflow(1, 2, 3)).result()  # 9
-    assert result == 9
-
-
-def test_comparison2(tmp_path, monkeypatch):
-    monkeypatch.chdir(tmp_path)
-
-    @job
-    def add(a, b):
-        return a + b
-
-    @job
-    def make_more(val):
-        return [val] * 3
-
-    @subflow  # (1)!
-    def add_distributed(vals, c):
-        return [add(val, c) for val in vals]
-
-    delayed1 = add(1, 2)
-    delayed2 = make_more(delayed1)
-    delayed3 = add_distributed(delayed2, 3)
-
-    assert dask.compute(*client.gather(delayed3)) == (6, 6, 6)
-
-
-def test_comparison3(tmp_path, monkeypatch):
-    monkeypatch.chdir(tmp_path)
-
-    @job  #  (1)!
-    def add(a, b):
-        return a + b
-
-    @job
-    def mult(a, b):
-        return a * b
-
-    delayed1 = add(1, 2)
-    delayed2 = mult(delayed1, 3)
-
-    assert client.compute(delayed2).result() == 9
diff --git a/tests/jobflow/.quacc.yaml b/tests/jobflow/.quacc.yaml
index c3fede4b..c77b66c7 100644
--- a/tests/jobflow/.quacc.yaml
+++ b/tests/jobflow/.quacc.yaml
@@ -1,3 +1,4 @@
+DEBUG: true
 RESULTS_DIR: ./.test_results
 SCRATCH_DIR: ./.test_scratch
 WORKFLOW_ENGINE: jobflow
diff --git a/tests/parsl/.quacc.yaml b/tests/parsl/.quacc.yaml
index d5484451..29741428 100644
--- a/tests/parsl/.quacc.yaml
+++ b/tests/parsl/.quacc.yaml
@@ -1,4 +1,5 @@
 CREATE_UNIQUE_DIR: true
+DEBUG: true
 RESULTS_DIR: ./.test_results
 SCRATCH_DIR: ./.test_scratch
 WORKFLOW_ENGINE: parsl
diff --git a/tests/parsl/test_parsl_recipes.py b/tests/parsl/test_parsl_recipes.py
index acb63e94..6323a36f 100644
--- a/tests/parsl/test_parsl_recipes.py
+++ b/tests/parsl/test_parsl_recipes.py
@@ -1,4 +1,5 @@
 import contextlib
+from functools import partial
 
 import pytest
 from ase.build import bulk
@@ -10,7 +11,9 @@ pytestmark = pytest.mark.skipif(
     SETTINGS.WORKFLOW_ENGINE != "parsl",
     reason="This test requires the Parsl workflow engine",
 )
+
 from quacc.recipes.emt.core import relax_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 
 def setup_module():
@@ -22,6 +25,19 @@ def teardown_module():
     parsl.clear()
 
 
+def test_parsl_functools(tmp_path, monkeypatch):
+    monkeypatch.chdir(tmp_path)
+    atoms = bulk("Cu")
+    result = bulk_to_slabs_flow(
+        atoms,
+        custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}),
+        run_static=False,
+    ).result()
+    assert len(result) == 4
+    assert "atoms" in result[-1]
+    assert result[-1]["fmax"] == 0.1
+
+
 def test_phonon_flow(tmp_path, monkeypatch):
     pytest.importorskip("phonopy")
     from quacc.recipes.emt.phonons import phonon_flow
diff --git a/tests/parsl/test_parsl_tutorials.py b/tests/parsl/test_parsl_tutorials.py
index 91094704..0afb564a 100644
--- a/tests/parsl/test_parsl_tutorials.py
+++ b/tests/parsl/test_parsl_tutorials.py
@@ -115,13 +115,7 @@ def test_tutorial2c(tmp_path, monkeypatch):
     # Define the workflow
     def workflow(atoms):
         relaxed_bulk = relax_job(atoms)
-        return bulk_to_slabs_flow(
-            relaxed_bulk["atoms"],
-            run_static=False,
-            # slab_relax_kwargs={
-            #     "opt_params": {"optimizer_kwargs": {"logfile": "-"}}
-            # },  # this is for easy debugging
-        )
+        return bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False)  # (1)!
 
     # Define the Atoms object
     atoms = bulk("Cu")
diff --git a/tests/redun/.quacc.yaml b/tests/redun/.quacc.yaml
index 8ab130af..e5892c4b 100644
--- a/tests/redun/.quacc.yaml
+++ b/tests/redun/.quacc.yaml
@@ -1,3 +1,4 @@
+DEBUG: true
 RESULTS_DIR: ./.test_results
 SCRATCH_DIR: ./.test_scratch
 WORKFLOW_ENGINE: redun
diff --git a/tests/redun/test_redun_recipes.py b/tests/redun/test_redun_recipes.py
new file mode 100644
index 00000000..3675ece7
--- /dev/null
+++ b/tests/redun/test_redun_recipes.py
@@ -0,0 +1,36 @@
+from functools import partial
+
+import pytest
+from ase.build import bulk
+
+from quacc import SETTINGS
+
+redun = pytest.importorskip("redun")
+pytestmark = pytest.mark.skipif(
+    SETTINGS.WORKFLOW_ENGINE != "redun",
+    reason="This test requires the Redun workflow engine",
+)
+
+
+@pytest.fixture()
+def scheduler():
+    return redun.Scheduler()
+
+
+from quacc.recipes.emt.core import relax_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
+
+
+def test_redun_functools(tmp_path, monkeypatch, scheduler):
+    monkeypatch.chdir(tmp_path)
+    atoms = bulk("Cu")
+    result = scheduler.run(
+        bulk_to_slabs_flow(
+            atoms,
+            custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}),
+            run_static=False,
+        )
+    )
+    assert len(result) == 4
+    assert "atoms" in result[-1]
+    assert result[-1]["fmax"] == 0.1