Repository: Quantum-Accelerators/quacc
Pull request #1358 (2023-12-13 19:54:31)

Use Pymatgen's Q-Chem scratch parser
Enabled by https://github.com/materialsproject/pymatgen/pull/3483. Waiting on the next release of pymatgen.


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 43bb7c7e..1d0a8888 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -4,11 +4,12 @@
 
 - [ ] 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
 
-- Your PR will likely not be merged without proper and thorough tests, even if it's for a "simple" fix.
+- Your PR will likely not be merged without proper and thorough tests.
 - If you are an external contributor, you will see a comment from [@buildbot-princeton](https://github.com/buildbot-princeton). This is solely for the maintainers.
 - When your code is ready for review, ping one of the [active maintainers](https://quantum-accelerators.github.io/quacc/about/contributors.html#active-maintainers).
 
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 1d7ebfb3..40d338d1 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -35,7 +35,7 @@ jobs:
           pip install .[docs]
 
       - name: Build docs
-        run: mkdocs build
+        run: mkdocs build --strict
         id: build_docs
 
       - name: Rebuild and deploy docs
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/dev/contributing.md b/docs/dev/contributing.md
index 0475036d..05c7fb12 100644
--- a/docs/dev/contributing.md
+++ b/docs/dev/contributing.md
@@ -58,7 +58,7 @@ In general, please try to keep the code style consistent when possible, particul
 
 2. Address any relevant issues raised by the GitHub Actions test suite.
 
-3. All Python code should be formatted with [isort](https://github.com/PyCQA/isort) (`isort .`), [black](https://github.com/psf/black) (`black .`), and [ruff](https://github.com/astral-sh/ruff) (`ruff . --fix`), although this will be corrected automatically when merged.
+3. All Python code should be formatted with [isort](https://github.com/PyCQA/isort) (`isort .`), [black](https://github.com/psf/black) (`black .`), and [ruff](https://github.com/astral-sh/ruff) (`ruff . --fix`).
 
 ## Unit Tests
 
diff --git a/docs/install/codes.md b/docs/install/codes.md
index 3eac82d7..febf2ed2 100644
--- a/docs/install/codes.md
+++ b/docs/install/codes.md
@@ -45,13 +45,13 @@ No setup needed!
 
 ## MLPs
 
-Several so-called "universal" machine-learned potentials (MLPs) are natively supported in quacc, including [U-MACE](https://github.com/ACEsuit/mace), [CHGNet](https://github.com/CederGroupHub/chgnet), and [M3GNet](https://github.com/materialsvirtuallab/matgl).
+Several pre-trained "universal" machine-learned potentials (MLPs) are natively supported in quacc, including those based on [MACE](https://github.com/ACEsuit/mace), [CHGNet](https://github.com/CederGroupHub/chgnet), and [M3GNet](https://github.com/materialsvirtuallab/matgl).
 
 To use these potentials, you will need to install the corresponding packages. This can be done as follows:
 
 ```bash
 pip install quacc[ml] # CHGNet, M3GNet
-pip install git+https://github.com/ACEsuit/mace.git # U-MACE
+pip install git+https://github.com/ACEsuit/mace.git # MACE
 ```
 
 ## NewtonNet
diff --git a/docs/user/basics/wflow_syntax.md b/docs/user/basics/wflow_syntax.md
index e4b9b166..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()` |
@@ -82,7 +82,7 @@ To help enable interoperability between workflow engines, quacc offers a unified
 
         Due to the difference in how Jobflow handles workflows compared to other supported workflow engines, any quacc recipes that have been pre-defined with a `#!Python @flow` or `#!Python @subflow` decorator (i.e. have `_flow` in the name) cannot be run directly with Jobflow.
 
-The quacc descriptors are drop-in replacements for the specified workflow engine analogue, which we will use for the remainder of the tutorials. Based on the value for the `WORKFLOW_ENGINE` global variable in your [quacc settings](../settings/settings.md), the appropriate decorator will be automatically selected. If the `WORKFLOW_ENGINE` setting is set to `None`, the decorators will have no effect on the underlying function.
+The quacc descriptors are drop-in replacements for the specified workflow engine analogue, which we will use for the remainder of the tutorials. Based on the value for the `WORKFLOW_ENGINE` global variable in your [quacc settings](../settings/settings.md), the appropriate decorator will be automatically selected. If the `WORKFLOW_ENGINE` setting is set to `None` (i.e. `quacc set WORKFLOW_ENGINE None`), the decorators will have no effect on the underlying function.
 
 ## Simple Workflow
 
diff --git a/docs/user/recipes/recipes_intro.md b/docs/user/recipes/recipes_intro.md
index 89e9f7ea..6121c6b6 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,46 @@ print(result2)
     'volume': 11.761470249999999}
     ```
 
+### Modifying Jobs in a Dynamic 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/docs/user/recipes/recipes_list.md b/docs/user/recipes/recipes_list.md
index b9f970bf..2d24016b 100644
--- a/docs/user/recipes/recipes_list.md
+++ b/docs/user/recipes/recipes_list.md
@@ -85,7 +85,7 @@ The list of available quacc recipes is shown below. The "Req'd Extras" column sp
 
 !!! Info
 
-    Machine-learned interatomic potentials are a class of machine learning models that are trained to reproduce the potential energy surface of a system. The following pre-trained, "universal" MLPs are supported: [CHGNet](https://doi.org/10.1038/s42256-023-00716-3), [M3GNet](https://doi.org/10.1038/s43588-022-00349-3), and [U-MACE](https://mace-docs.readthedocs.io/en/latest/examples/foundation_models.html).
+    Machine-learned interatomic potentials are a class of machine learning models that are trained to reproduce the potential energy surface of a system. The following pre-trained, "universal" MLPs are supported: [CHGNet](https://doi.org/10.1038/s42256-023-00716-3), [M3GNet](https://doi.org/10.1038/s43588-022-00349-3), and [MACE](https://mace-docs.readthedocs.io/en/latest/examples/foundation_models.html).
 
 <center>
 
@@ -208,8 +208,8 @@ The list of available quacc recipes is shown below. The "Req'd Extras" column sp
 | VASP Static             | `#!Python @job`  | [quacc.recipes.vasp.core.static_job][]          |              |
 | VASP Relax              | `#!Python @job`  | [quacc.recipes.vasp.core.relax_job][]           |              |
 | VASP Double Relax       | `#!Python @job`  | [quacc.recipes.vasp.core.double_relax_job][]    |              |
-| VASP Slab Static        | `#!Python @job`  | [quacc.recipes.vasp.slabs.slab_static_job][]    |              |
-| VASP Slab Relax         | `#!Python @job`  | [quacc.recipes.vasp.slabs.slab_relax_job][]     |              |
+| VASP Slab Static        | `#!Python @job`  | [quacc.recipes.vasp.slabs.static_job][]         |              |
+| VASP Slab Relax         | `#!Python @job`  | [quacc.recipes.vasp.slabs.relax_job][]          |              |
 | VASP Bulk to Slabs      | `#!Python @flow` | [quacc.recipes.vasp.slabs.bulk_to_slabs_flow][] |              |
 | VASP Slab to Adsorbates | `#!Python @flow` | [quacc.recipes.vasp.slabs.slab_to_ads_flow][]   |              |
 | VASP MP Prerelax        | `#!Python @job`  | [quacc.recipes.vasp.mp.mp_relax_job][]          |              |
diff --git a/docs/user/settings/settings.md b/docs/user/settings/settings.md
index 2c921cd9..db4726a9 100644
--- a/docs/user/settings/settings.md
+++ b/docs/user/settings/settings.md
@@ -15,11 +15,14 @@ If you are planning to modify a given set of parameters for all of your calculat
 ```yaml title="~/.quacc.yaml"
 SCRATCH_DIR: $SCRATCH # (1)!
 CREATE_UNIQUE_DIR: true # (2)!
+WORKFLOW_ENGINE: # (3)!
 ```
 
-1. This would set the quacc scratch directory to the environment variable `$SCRATCH` on whatever machine the calculations are run on.
+1. In YAML, you do not need to put quotation marks around strings. In this case, the quacc scratch directory would be set to the environment variable `$SCRATCH` on whatever machine the calculations are run on.
 
-2. This would ensure that each job in your quacc workflow is stored in a unique, isolated directory.
+2. In YAML, booleans are lowercase. In this case, the setting would ensure that each job in your quacc workflow is stored in a unique, isolated directory.
+
+3. In YAML, a blank value is interpreted as `None` in Python. A value of `null` is equivalent to `None` too. In this case, no workflow engine would be used.
 
 !!! Tip "Command-Line Interface"
 
diff --git a/docs/user/wflow_engine/executors2.md b/docs/user/wflow_engine/executors2.md
index 76c94709..dc14b846 100644
--- a/docs/user/wflow_engine/executors2.md
+++ b/docs/user/wflow_engine/executors2.md
@@ -343,7 +343,7 @@ When deploying calculations for the first time, it's important to start simple,
 
 ## Example 2
 
-In this example, we will run a sample VASP recipe that will highlight the use of a more complicated configuration.
+In this example, we will run a sample VASP recipe that will highlight the use of a more complicated configuration. It can only be run if you are a licensed VASP user, but the same fundamental principles apply to many other DFT codes with recipes in quacc.
 
 First, prepare your `QUACC_VASP_PP_PATH` environment variable in the `~/.bashrc` of your remote machine as described in the [Calculator Setup guide](../../install/codes.md). When you're done, follow the steps below.
 
diff --git a/mkdocs.yml b/mkdocs.yml
index c07afbc0..b3c1fec3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -25,7 +25,7 @@ nav:
           - user/wflow_engine/executors2.md
       - Global Settings:
           - user/settings/settings.md
-          - user/advanced/file_management.md
+          - user/settings/file_management.md
           - user/settings/settings_list.md
       - Advanced:
           - user/advanced/database.md
diff --git a/pyproject.toml b/pyproject.toml
index 5403667c..3411e379 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
 [project]
 name = "quacc"
 description="A platform to enable high-throughput, database-driven quantum chemistry and computational materials science"
-version = "0.4.6"
+version = "0.4.7"
 readme = "README.md"
 license = { text = "BSD-3" }
 authors = [{ name = "Andrew S. Rosen", email = "asrosen@princeton.edu" }]
@@ -37,13 +37,13 @@ dependencies = [
     "psutil", # for getting compute architecture details
     "pydantic>=2.0.1", # for settings management
     "pydantic-settings>=2.0.3", # for settings management
-    "pymatgen>=2023.10.4", # for structure manipulation
+    "pymatgen>=2023.12.18", # for structure manipulation
     "typer>=0.9.0", # for the CLI
 ]
 
 [project.optional-dependencies]
 covalent = ["covalent>=0.226.0rc0", "covalent-hpc-plugin>=0.0.3"]
-dask = ["dask[distributed]>=2023.12.0", "dask-jobqueue>=0.8.2"]
+dask = ["dask[distributed]>=2023.12.1", "dask-jobqueue>=0.8.2"]
 defects = ["pymatgen-analysis-defects>=2023.8.22", "shakenbreak>=3.2.0"]
 jobflow = ["jobflow>=0.1.14", "fireworks>=2.0.3"]
 mlp = ["matgl>=0.9.1", "chgnet>=0.3.3"]
@@ -150,6 +150,7 @@ extend-ignore = [
   "EM102",  # Exception must not use an f-string literal
   "G004",   # f-string in Logging statement
   "RET503", # Missing explicit return
+  "RUF015", # Prefer next(iter())
 ]
 typing-modules = ["mypackage._compat.typing"]
 src = ["src"]
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/_cli/quacc.py b/src/quacc/_cli/quacc.py
index 3f80c9e1..a5de0f47 100644
--- a/src/quacc/_cli/quacc.py
+++ b/src/quacc/_cli/quacc.py
@@ -177,7 +177,7 @@ def _type_handler(value: str) -> Any:
     -------
     Any
     """
-    if value.lower() in ("null", "none"):
+    if value.lower() in {"null", "none"}:
         value = None
     elif value.lower() in ("true", "false"):
         value = value.lower() == "true"
diff --git a/src/quacc/atoms/defects.py b/src/quacc/atoms/defects.py
index 7b2a942b..a4794bac 100644
--- a/src/quacc/atoms/defects.py
+++ b/src/quacc/atoms/defects.py
@@ -145,11 +145,10 @@ def _get_defect_entry_from_defect(
         defect entry
     """
     # Find defect's fractional coordinates and remove it from supercell
-    dummy_site = next(
-        site
-        for site in defect_supercell
-        if site.species.elements[0].symbol == DummySpecies().symbol
-    )
+    for site in defect_supercell:
+        if site.species.elements[0].symbol == DummySpecies().symbol:
+            dummy_site = site
+            break
     defect_supercell.remove(dummy_site)
 
     computed_structure_entry = ComputedStructureEntry(
diff --git a/src/quacc/calculators/qchem/io.py b/src/quacc/calculators/qchem/io.py
index d2424b8a..2d5c963a 100644
--- a/src/quacc/calculators/qchem/io.py
+++ b/src/quacc/calculators/qchem/io.py
@@ -5,15 +5,18 @@ import struct
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-import numpy as np
 from ase import units
 from emmet.core.tasks import _parse_custodian
-from monty.io import zopen
 from pymatgen.io.qchem.inputs import QCInput
-from pymatgen.io.qchem.outputs import QCOutput
+from pymatgen.io.qchem.outputs import (
+    QCOutput,
+    gradient_parser,
+    hessian_parser,
+    orbital_coeffs_parser,
+)
 
 if TYPE_CHECKING:
-    from quacc.calculators.qchem.qchem import Results
+    from quacc.calculators._qchem_beta.qchem import Results
 
 
 def write_qchem(
@@ -75,70 +78,33 @@ def read_qchem(directory: Path | str = ".") -> tuple[Results, list[float]]:
         "custodian": _parse_custodian(directory),
     }
 
+    # Parse thermo properties
+    if "total_enthalpy" in qc_output:
+        results["enthalpy"] = qc_output["total_enthalpy"] * (units.kcal / units.mol)
+    if "total_entropy" in qc_output:
+        results["entropy"] = qc_output["total_entropy"] * (
+            0.001 * units.kcal / units.mol
+        )
+
     # Read the gradient scratch file in 8 byte chunks
     grad_scratch = directory / "131.0"
     if grad_scratch.exists() and grad_scratch.stat().st_size > 0:
-        tmp_grad_data = []
-        with zopen(grad_scratch, mode="rb") as file:
-            binary = file.read()
-        tmp_grad_data.extend(
-            struct.unpack("d", binary[ii * 8 : (ii + 1) * 8])[0]
-            for ii in range(len(binary) // 8)
-        )
-        grad = [
-            [
-                float(tmp_grad_data[ii * 3]),
-                float(tmp_grad_data[ii * 3 + 1]),
-                float(tmp_grad_data[ii * 3 + 2]),
-            ]
-            for ii in range(len(tmp_grad_data) // 3)
-        ]
-
-        # Ensure that the scratch values match the correct values from the
-        # output file but with higher precision
-        if qc_output["pcm_gradients"] is not None:
-            gradient = qc_output["pcm_gradients"][0]
-        else:
-            gradient = qc_output["gradients"][0]
-        tol = 1e-6
-        for ii, subgrad in enumerate(grad):
-            for jj, val in enumerate(subgrad):
-                if abs(gradient[ii, jj] - val) > tol:
-                    raise ValueError(
-                        "Difference between gradient value in scratch file vs. output file should not be this large."
-                    )
-                gradient[ii, jj] = val
-
-        # Convert gradient to force + deal with units
+        gradient = gradient_parser(directory / "131.0")
+
         results["forces"] = gradient * (-units.Hartree / units.Bohr)
 
     # Read Hessian scratch file in 8 byte chunks
     hessian_scratch = directory / "132.0"
     if hessian_scratch.exists() and hessian_scratch.stat().st_size > 0:
-        tmp_hess_data = []
-        with zopen(hessian_scratch, mode="rb") as file:
-            binary = file.read()
-        tmp_hess_data.extend(
-            struct.unpack("d", binary[ii * 8 : (ii + 1) * 8])[0]
-            for ii in range(len(binary) // 8)
-        )
-        reshaped_hess = np.reshape(
-            np.array(tmp_hess_data),
-            (len(qc_output["species"]) * 3, len(qc_output["species"]) * 3),
+        reshaped_hess = hessian_parser(
+            hessian_scratch, n_atoms=len(qc_output["species"])
         )
         results["hessian"] = reshaped_hess * (units.Hartree / units.Bohr**2)
-        results["enthalpy"] = qc_output["total_enthalpy"] * (units.kcal / units.mol)
-        results["entropy"] = qc_output["total_entropy"] * (
-            0.001 * units.kcal / units.mol
-        )
 
     # Read orbital coefficients scratch file in 8 byte chunks
-    prev_orbital_coeffs = []
-    with zopen(directory / "53.0", mode="rb") as file:
-        binary = file.read()
-    prev_orbital_coeffs.extend(
-        struct.unpack("d", binary[ii * 8 : (ii + 1) * 8])[0]
-        for ii in range(len(binary) // 8)
-    )
+    orb_scratch = directory / "53.0"
+    prev_orbital_coeffs = None
+    if orb_scratch.exists() and orb_scratch.stat().st_size > 0:
+        prev_orbital_coeffs = orbital_coeffs_parser(orb_scratch).tolist()
 
     return results, prev_orbital_coeffs
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..4d5b675e 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.
+    custom_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/espresso/core.py b/src/quacc/recipes/espresso/core.py
index e3e9ab43..e862bd9c 100644
--- a/src/quacc/recipes/espresso/core.py
+++ b/src/quacc/recipes/espresso/core.py
@@ -47,7 +47,7 @@ def static_job(
         calculation. See the ASE documentation for more information.
     **calc_kwargs
         Additional keyword arguments to pass to the Espresso calculator. See the
-        docstring of [][ase.calculators.espresso.espresso] for more information.
+        docstring of [][quacc.calculators.espresso.espresso.Espresso] for more information.
 
     Returns
     -------
@@ -113,8 +113,7 @@ def phonon_job(
         - qpts: list[list[float]] | list[tuple[float]] | list[float]
         - nat_todo: list[int]
 
-        See the docstring of [quacc.calculators.espresso.espresso.io.write_espresso_ph][] for more
-        information.
+        See the docstring of [quacc.calculators.espresso.io.write_espresso_ph][] for more information.
 
     Returns
     -------
diff --git a/src/quacc/recipes/mlp/_base.py b/src/quacc/recipes/mlp/_base.py
index edb024fb..f6c7f817 100644
--- a/src/quacc/recipes/mlp/_base.py
+++ b/src/quacc/recipes/mlp/_base.py
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
 
 @lru_cache
 def _pick_calculator(
-    calculator: Literal["umace", "m3gnet", "chgnet"], **kwargs
+    calculator: Literal["mace", "m3gnet", "chgnet"], **kwargs
 ) -> Calculator:
     """
     Adapted from `matcalc.util.get_universal_calculator`.
@@ -48,7 +48,7 @@ def _pick_calculator(
 
         calc = CHGNetCalculator(**kwargs)
 
-    elif calculator.lower() == "umace":
+    elif calculator.lower() == "mace":
         from mace import __version__
         from mace.calculators import mace_mp
 
diff --git a/src/quacc/recipes/mlp/core.py b/src/quacc/recipes/mlp/core.py
index fb1e18ec..2fa0da10 100644
--- a/src/quacc/recipes/mlp/core.py
+++ b/src/quacc/recipes/mlp/core.py
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
 
 @job
 def static_job(
-    atoms: Atoms, method: Literal["umace", "m3gnet", "chgnet"], **calc_kwargs
+    atoms: Atoms, method: Literal["mace", "m3gnet", "chgnet"], **calc_kwargs
 ) -> RunSchema:
     """
     Carry out a single-point calculation.
@@ -53,7 +53,7 @@ def static_job(
 @job
 def relax_job(
     atoms: Atoms,
-    method: Literal["umace", "m3gnet", "chgnet"],
+    method: Literal["mace", "m3gnet", "chgnet"],
     relax_cell: bool = False,
     opt_params: dict[str, Any] | None = None,
     **calc_kwargs,
diff --git a/src/quacc/recipes/orca/core.py b/src/quacc/recipes/orca/core.py
index 07e7d1f7..41ceb422 100644
--- a/src/quacc/recipes/orca/core.py
+++ b/src/quacc/recipes/orca/core.py
@@ -92,8 +92,8 @@ def static_job(
 @job
 def relax_job(
     atoms: Atoms,
-    charge: int,
-    spin_multiplicity: int,
+    charge: int = 0,
+    spin_multiplicity: int = 1,
     xc: str = "wb97x-d3bj",
     basis: str = "def2-tzvp",
     run_freq: bool = False,
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/qmof.py b/src/quacc/recipes/vasp/qmof.py
index 07b58681..a922e263 100644
--- a/src/quacc/recipes/vasp/qmof.py
+++ b/src/quacc/recipes/vasp/qmof.py
@@ -60,7 +60,7 @@ def qmof_relax_job(
         If True, a pre-relax will be carried out with BFGSLineSearch.
         Recommended if starting from hypothetical structures or materials with
         very high starting forces.
-    **kwargs
+    **calc_kwargs
         Custom kwargs for the calculator. Set a value to `None` to remove
         a pre-existing key entirely. Applies for all jobs.
 
diff --git a/src/quacc/recipes/vasp/slabs.py b/src/quacc/recipes/vasp/slabs.py
index 47ea25a6..412c6ce2 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.relax_job][].
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.vasp.slabs.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.relax_job][].
+    custom_static_job
+        Static job, which defaults to [quacc.recipes.vasp.slabs.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 86f3ec49..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,8 +322,25 @@ 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"""
+        return None if v and v.lower() == "local" else v
+
     @field_validator("RESULTS_DIR", "SCRATCH_DIR")
     @classmethod
     def resolve_and_make_paths(cls, v: Optional[Path]) -> Optional[Path]:
@@ -358,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 ab5bbeb2..f45a1be4 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -47,7 +47,7 @@ jobs:
         run: pytest --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} ${{ matrix.python-version }} ${{ matrix.os }} coverage report
           path: "coverage.xml"
@@ -76,7 +76,8 @@ jobs:
       - name: Install pip packages
         run: |
           pip install -r tests/requirements.txt
-          pip install -r tests/covalent/requirements-covalent.txt
+          pip install -r tests/requirements-covalent.txt
+          pip install -r tests/phonopy.txt
           pip install .[dev]
 
       - name: Start Covalent server
@@ -89,7 +90,7 @@ jobs:
         run: covalent stop
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -120,14 +121,15 @@ jobs:
       - name: Install pip packages
         run: |
           pip install -r tests/requirements.txt
-          pip install -r tests/${{ matrix.wflow_engine }}/requirements-${{ matrix.wflow_engine }}.txt
+          pip install -r tests/requirements-${{ matrix.wflow_engine }}.txt
+          pip install -r tests/phonopy.txt
           pip install .[dev]
 
       - name: Run tests with pytest
         run: pytest --durations=10 tests/${{ matrix.wflow_engine }} --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} ${{ matrix.wflow_engine }} coverage report
           path: "coverage.xml"
@@ -173,7 +175,7 @@ jobs:
         run: pytest -k 'psi4' --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -219,7 +221,7 @@ jobs:
         run: pytest -k 'defects or phonon or espresso' --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -265,7 +267,7 @@ jobs:
         run: pytest -k 'dftb or tblite' --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -312,7 +314,7 @@ jobs:
         run: pytest -k 'qchem or sella' --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -355,7 +357,7 @@ jobs:
         run: pytest -k 'mlp or newtonnet' --durations=10 --cov=quacc --cov-report=xml
 
       - name: Upload code coverage report to Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ github.job }} coverage report
           path: "coverage.xml"
@@ -376,13 +378,13 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Download coverage artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
 
       - name: Upload coverage to Codecov
         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/README.md b/tests/README.md
index 12958547..1e75a11e 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -20,4 +20,4 @@ pytest tests/parsl # or `pytest .` from within `tests/parsl`
 
 ## Troubleshooting Tests
 
-If you ever need to inspect files from recipes that are generated during a test run, you can find them in the `.quacc_scratch` and `.quacc_results` directories that are created in the current working directory.
+If you ever need to inspect files from a failed test run, you can find them in the `.quacc_scratch` and `.quacc_results` directories that are created in the current working directory.
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/mlp_recipes/test_core_recipes.py b/tests/core/recipes/mlp_recipes/test_core_recipes.py
index c21144df..69794c9c 100644
--- a/tests/core/recipes/mlp_recipes/test_core_recipes.py
+++ b/tests/core/recipes/mlp_recipes/test_core_recipes.py
@@ -12,19 +12,19 @@ def test_bad_method():
         static_job(atoms, method="bad_method")
 
 
-@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "umace"])
+@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "mace"])
 def test_static_job(tmp_path, monkeypatch, method):
     monkeypatch.chdir(tmp_path)
     ref_energy = {
         "chgnet": -4.083308219909668,
         "m3gnet": -4.0938973,
-        "umace": -4.083906650543213,
+        "mace": -4.083906650543213,
     }
     if method == "chgnet":
         pytestmark = pytest.importorskip("chgnet")
     elif method == "m3gnet":
         pytestmark = pytest.importorskip("matgl")
-    elif method == "umace":
+    elif method == "mace":
         pytestmark = pytest.importorskip("mace")
     atoms = bulk("Cu")
     output = static_job(atoms, method=method)
@@ -33,19 +33,19 @@ def test_static_job(tmp_path, monkeypatch, method):
     assert output["atoms"] == atoms
 
 
-@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "umace"])
+@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "mace"])
 def test_relax_job(tmp_path, monkeypatch, method):
     monkeypatch.chdir(tmp_path)
     ref_energy = {
         "chgnet": -32.662731,
         "m3gnet": -32.747219,
-        "umace": -32.66752624511719,
+        "mace": -32.66752624511719,
     }
     if method == "chgnet":
         pytestmark = pytest.importorskip("chgnet")
     elif method == "m3gnet":
         pytestmark = pytest.importorskip("matgl")
-    elif method == "umace":
+    elif method == "mace":
         pytestmark = pytest.importorskip("mace")
 
     atoms = bulk("Cu") * (2, 2, 2)
@@ -57,19 +57,19 @@ def test_relax_job(tmp_path, monkeypatch, method):
     assert output["atoms"].get_volume() == pytest.approx(atoms.get_volume())
 
 
-@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "umace"])
+@pytest.mark.parametrize("method", ["chgnet", "m3gnet", "mace"])
 def test_relax_cell_job(tmp_path, monkeypatch, method):
     monkeypatch.chdir(tmp_path)
     ref_energy = {
         "chgnet": -32.664215087890625,
         "m3gnet": -32.74749,
-        "umace": -32.67439270019531,
+        "mace": -32.67439270019531,
     }
     if method == "chgnet":
         pytestmark = pytest.importorskip("chgnet")
     elif method == "m3gnet":
         pytestmark = pytest.importorskip("matgl")
-    elif method == "umace":
+    elif method == "mace":
         pytestmark = pytest.importorskip("mace")
 
     atoms = bulk("Cu") * (2, 2, 2)
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 a84f67c3..dd270be4 100644
--- a/tests/core/settings/test_settings.py
+++ b/tests/core/settings/test_settings.py
@@ -29,15 +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")
+        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 d324a73f..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
 
@@ -9,7 +11,23 @@ pytestmark = pytest.mark.skipif(
     reason="This test requires the Covalent workflow engine",
 )
 
-from quacc.recipes.emt.core import relax_job
+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/covalent/test_covalent_tutorials.py b/tests/covalent/test_covalent_tutorials.py
index 191db806..609c4508 100644
--- a/tests/covalent/test_covalent_tutorials.py
+++ b/tests/covalent/test_covalent_tutorials.py
@@ -10,8 +10,8 @@ pytestmark = pytest.mark.skipif(
 )
 
 
-from quacc.recipes.emt.core import relax_job, static_job
-from quacc.recipes.emt.slabs import bulk_to_slabs_flow
+from quacc.recipes.emt.core import relax_job, static_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 
 def test_quickstart(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/requirements-dask.txt b/tests/dask/requirements-dask.txt
deleted file mode 100644
index cfa72c91..00000000
--- a/tests/dask/requirements-dask.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-dask[distributed]==2023.12.0
-dask-jobqueue==0.8.2
-phonopy==2.21.0
diff --git a/tests/dask/test_dask_recipes.py b/tests/dask/test_dask_recipes.py
index c67b89ed..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
 
@@ -11,11 +13,30 @@ pytestmark = pytest.mark.skipif(
 
 from dask.distributed import default_client
 
-from quacc.recipes.emt.core import relax_job
+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 6fd5cd0e..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(
@@ -10,8 +10,8 @@ pytestmark = pytest.mark.skipif(
 )
 from dask.distributed import default_client
 
-from quacc.recipes.emt.core import relax_job, static_job
-from quacc.recipes.emt.slabs import bulk_to_slabs_flow
+from quacc.recipes.emt.core import relax_job, static_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 client = default_client()
 
@@ -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/jobflow/test_jobflow_tutorials.py b/tests/jobflow/test_jobflow_tutorials.py
index 40a3ca8b..55455613 100644
--- a/tests/jobflow/test_jobflow_tutorials.py
+++ b/tests/jobflow/test_jobflow_tutorials.py
@@ -8,7 +8,7 @@ pytestmark = pytest.mark.skipif(
     SETTINGS.WORKFLOW_ENGINE != "jobflow",
     reason="This test requires the Jobflow workflow engine",
 )
-from quacc.recipes.emt.core import relax_job, static_job
+from quacc.recipes.emt.core import relax_job, static_job  # skipcq: PYL-C0412
 
 
 def test_tutorial1a(tmp_path, monkeypatch):
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 6ff64df9..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
+
+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 47e0886b..0afb564a 100644
--- a/tests/parsl/test_parsl_tutorials.py
+++ b/tests/parsl/test_parsl_tutorials.py
@@ -12,8 +12,8 @@ pytestmark = pytest.mark.skipif(
 
 from ase.build import bulk, molecule
 
-from quacc.recipes.emt.core import relax_job, static_job
-from quacc.recipes.emt.slabs import bulk_to_slabs_flow
+from quacc.recipes.emt.core import relax_job, static_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 
 def setup_module():
@@ -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
diff --git a/tests/redun/test_redun_tutorials.py b/tests/redun/test_redun_tutorials.py
index 14c94a76..1a2e4c16 100644
--- a/tests/redun/test_redun_tutorials.py
+++ b/tests/redun/test_redun_tutorials.py
@@ -9,8 +9,8 @@ pytestmark = pytest.mark.skipif(
     reason="This test requires the Redun workflow engine",
 )
 
-from quacc.recipes.emt.core import relax_job, static_job
-from quacc.recipes.emt.slabs import bulk_to_slabs_flow
+from quacc.recipes.emt.core import relax_job, static_job  # skipcq: PYL-C0412
+from quacc.recipes.emt.slabs import bulk_to_slabs_flow  # skipcq: PYL-C0412
 
 
 @pytest.fixture()
diff --git a/tests/covalent/requirements-covalent.txt b/tests/requirements-covalent.txt
similarity index 76%
rename from tests/covalent/requirements-covalent.txt
rename to tests/requirements-covalent.txt
index 8c81e8c0..1b89cd05 100644
--- a/tests/covalent/requirements-covalent.txt
+++ b/tests/requirements-covalent.txt
@@ -1,3 +1,2 @@
 covalent==0.227.0rc0
 covalent-hpc-plugin==0.0.8
-phonopy==2.21.0
\ No newline at end of file
diff --git a/tests/requirements-dask.txt b/tests/requirements-dask.txt
new file mode 100644
index 00000000..cb85ff10
--- /dev/null
+++ b/tests/requirements-dask.txt
@@ -0,0 +1,2 @@
+dask[distributed]==2023.12.1
+dask-jobqueue==0.8.2
diff --git a/tests/requirements-defects.txt b/tests/requirements-defects.txt
index a371a46a..dd4c419f 100644
--- a/tests/requirements-defects.txt
+++ b/tests/requirements-defects.txt
@@ -1,2 +1,2 @@
-pymatgen-analysis-defects==2023.10.19
+pymatgen-analysis-defects==2023.12.14
 shakenbreak==3.2.3
\ No newline at end of file
diff --git a/tests/jobflow/requirements-jobflow.txt b/tests/requirements-jobflow.txt
similarity index 100%
rename from tests/jobflow/requirements-jobflow.txt
rename to tests/requirements-jobflow.txt
diff --git a/tests/parsl/requirements-parsl.txt b/tests/requirements-parsl.txt
similarity index 64%
rename from tests/parsl/requirements-parsl.txt
rename to tests/requirements-parsl.txt
index aef36b95..38c246b4 100644
--- a/tests/parsl/requirements-parsl.txt
+++ b/tests/requirements-parsl.txt
@@ -1,2 +1,1 @@
 parsl[monitoring]==2023.12.4
-phonopy==2.21.0
diff --git a/tests/redun/requirements-redun.txt b/tests/requirements-redun.txt
similarity index 100%
rename from tests/redun/requirements-redun.txt
rename to tests/requirements-redun.txt
diff --git a/tests/requirements.txt b/tests/requirements.txt
index d541d040..06c9a39d 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,11 +1,11 @@
 git+https://gitlab.com/ase/ase.git
 cclib==1.8
 custodian==2023.10.9
-emmet-core==0.74.8
-maggma==0.59.0
+emmet-core==0.75.0
+maggma==0.60.0
 monty==2023.11.3
 numpy==1.26.2
-psutil==5.9.6
+psutil==5.9.7
 pydantic==2.5.2
 pydantic-settings==2.1.0
-pymatgen==2023.11.12
+pymatgen==2023.12.18