Repository: BQSKit/bqskit (language: python)

Issue:
Compiler update

Reproducible circuits
Hi

I am trying to get reproducible circuits from BQSKit for the same qiskit circuit but I keep getting circuits with different depths and gate counts. I have tried setting a seed using both `seed_random_sources` and `SetRandomSeedPass` but I am unable to reproduce the same results each time. 

Is there any way I can achieve this using BQSKit?

This is what I am trying to run:
```python
seed_random_sources(0)
...
task = CompilationTask(
    bqskit_circuit, [
        SetRandomSeedPass(seed=0),
        QuickPartitioner(),
        ForEachBlockPass([QSearchSynthesisPass(), ScanningGateRemovalPass()]),
        UnfoldPass()
    ],
)

compiler = Compiler()
out_circuit = compiler.compile(task)
```


Discussion / hints:

Thanks for the bug report. The `SetRandomSeedPass` should handle this as you would expect; however, it has been a while since we implemented it.

To try and understand what is going wrong, can you try setting the seed at the instantiation level by changing the associated line to the following:

```
ForEachBlockPass([QSearchSynthesisPass(instantiate_options={'seed':0}), ScanningGateRemovalPass(instantiate_options={'seed':0})]),
```

If this works, there is a disconnect in the passes. Otherwise, something more serious is happening.
Thanks. I think this worked, the output is a more consistent that before. 
I didn't get time to verify programatically if the circuits are identical. But it seems like they are. 
I am glad to hear that this workaround works. Thanks for checking.

The original workflow you provided should have this expected behavior without setting the instantiate option; I will continue investigating and update here.
The problem is that the SetRandomSeedPass only runs on one dask worker. So with small jobs, everything is on one worker, and everything works as expected. However, with bigger jobs, only one worker has its seed set. To solve this, we need to tweak how we pass instantiation options between passes, which we had planned to do in the 1.1 update. For now, we have a workaround that works, and I will add a failing test in the 1.0.4 update to keep track of this in the future.

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d3444a265..6b55267cb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -85,4 +85,5 @@ repos:
         args:
             - --ignore=E741,E241,E226,F405,F403,W503,F901,E731,TYP006
             - --max-line-length=80
+            - --per-file-ignores=bqskit/runtime/worker.py:E402
         additional_dependencies: [flake8-typing-imports==1.13.0]
diff --git a/bqskit/__init__.py b/bqskit/__init__.py
index 09a2fccd9..3c6b81fdc 100644
--- a/bqskit/__init__.py
+++ b/bqskit/__init__.py
@@ -11,15 +11,7 @@
 from bqskit.ir.lang.qasm2 import OPENQASM2Language as _qasm
 
 # Initialize Logging
-_logger = logging.getLogger('bqskit')
-_handler = logging.StreamHandler(_stdout)
-_handler.setLevel(0)
-_fmt_header = '(%(thread)d): %(asctime)s.%(msecs)03d - %(levelname)-8s |'
-_fmt_message = ' %(name)s: %(message)s'
-_fmt = _fmt_header + _fmt_message
-_formatter = logging.Formatter(_fmt, '%H:%M:%S')
-_handler.setFormatter(_formatter)
-_logger.addHandler(_handler)
+_logging_initialized = False
 
 
 def enable_logging(verbose: bool = False) -> None:
@@ -30,42 +22,63 @@ def enable_logging(verbose: bool = False) -> None:
         verbose (bool): If set to True, will print more verbose messages.
             Defaults to False.
     """
+    global _logging_initialized
+    if not _logging_initialized:
+        _logger = logging.getLogger('bqskit')
+        _handler = logging.StreamHandler(_stdout)
+        _handler.setLevel(0)
+        _fmt_header = '%(asctime)s.%(msecs)03d - %(levelname)-8s |'
+        _fmt_message = ' %(name)s: %(message)s'
+        _fmt = _fmt_header + _fmt_message
+        _formatter = logging.Formatter(_fmt, '%H:%M:%S')
+        _handler.setFormatter(_formatter)
+        _logger.addHandler(_handler)
+        _logging_initialized = True
+
     level = logging.DEBUG if verbose else logging.INFO
     logging.getLogger('bqskit').setLevel(level)
 
 
-def disable_parallelism() -> None:
-    """
-    Disables the default parallelism in BQSKit.
-
-    If parallelism is disabled, everything will run single threaded. Performance
-    will degrade drastically but will free up computer resources. This setting
-    is ignored if connecting to an external cluster.
-    """
-    from bqskit.compiler import Compiler
-    Compiler._parallelism_enabled = False
+def enable_dashboard() -> None:
+    import warnings
+    warnings.warn(
+        'Dask has been removed from BQSKit. As a result, the'
+        ' enable_dashboard method has been removed.'
+        'This warning will turn into an error in a future update.',
+        DeprecationWarning,
+    )
 
 
-def enable_parallelism() -> None:
-    """
-    Enables parallelism; it is enabled by default.
-
-    See `disable_parallelism` for more info.
-    """
-    from bqskit.compiler import Compiler
-    Compiler._parallelism_enabled = True
+def disable_dashboard() -> None:
+    import warnings
+    warnings.warn(
+        'Dask has been removed from BQSKit. As a result, the'
+        ' disable_dashboard method has been removed.'
+        'This warning will turn into an error in a future update.',
+        DeprecationWarning,
+    )
 
 
-def enable_dashboard() -> None:
-    """Enables the dask dashboard."""
-    from bqskit.compiler import Compiler
-    Compiler._dashboard = True
+def disable_parallelism() -> None:
+    import warnings
+    warnings.warn(
+        'The disable_parallelism method has been removed.'
+        ' Instead, set the "num_workers" parameter to 1 during '
+        'Compiler construction. This warning will turn into'
+        'an error in a future update.',
+        DeprecationWarning,
+    )
 
 
-def disable_dashboard() -> None:
-    """Enables the dask dashboard."""
-    from bqskit.compiler import Compiler
-    Compiler._dashboard = False
+def enable_parallelism() -> None:
+    import warnings
+    warnings.warn(
+        'The enable_parallelism method has been removed.'
+        ' Instead, set the "num_workers" parameter to 1 during '
+        'Compiler construction. This warning will turn into'
+        'an error in a future update.',
+        DeprecationWarning,
+    )
 
 
 __all__ = [
diff --git a/bqskit/compiler/__init__.py b/bqskit/compiler/__init__.py
index ccac308d1..dbfb4b17f 100644
--- a/bqskit/compiler/__init__.py
+++ b/bqskit/compiler/__init__.py
@@ -34,7 +34,6 @@
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.compile import compile
 from bqskit.compiler.compiler import Compiler
-from bqskit.compiler.executor import Executor
 from bqskit.compiler.machine import MachineModel
 from bqskit.compiler.task import CompilationTask
 
@@ -42,7 +41,6 @@
     'BasePass',
     'compile',
     'Compiler',
-    'Executor',
     'MachineModel',
     'CompilationTask',
 ]
diff --git a/bqskit/compiler/basepass.py b/bqskit/compiler/basepass.py
index 44da2a0bb..6138bcd86 100644
--- a/bqskit/compiler/basepass.py
+++ b/bqskit/compiler/basepass.py
@@ -2,25 +2,19 @@
 from __future__ import annotations
 
 import abc
-import logging
-from typing import Any
-from typing import Callable
-from typing import TypeVar
+import warnings
+from typing import TYPE_CHECKING
 
-from distributed import get_client
-from distributed import rejoin
-from distributed import secede
-
-from bqskit.compiler.machine import MachineModel
-from bqskit.ir.circuit import Circuit
-from bqskit.qis.graph import CouplingGraph
-from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
-from bqskit.utils.typing import is_iterable
-from bqskit.utils.typing import is_sequence
-
-_logger = logging.getLogger(__name__)
-
-T = TypeVar('T')
+if TYPE_CHECKING:
+    from typing import Any
+    from bqskit.compiler.machine import MachineModel
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import Workflow
+    from bqskit.ir.circuit import Circuit
+    from bqskit.qis.graph import CouplingGraph
+    from bqskit.qis.state.system import StateSystem
+    from bqskit.qis.state.state import StateVector
+    from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
 class BasePass(abc.ABC):
@@ -33,7 +27,7 @@ class BasePass(abc.ABC):
 
     Examples:
         >>> class PrintCNOTCountPass(BasePass):
-        ...     def run(self, circ: Circuit, data: dict[str, Any] = {}) -> None:
+        ...     def run(self, circ: Circuit, }one:
         ...         print(f"Number of CNOTs: {circ.count(CNOTGate())}")
     """
 
@@ -43,14 +37,14 @@ def name(self) -> str:
         return self.__class__.__name__
 
     @abc.abstractmethod
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """
         Perform the pass's operation on `circuit`.
 
         Args:
             circuit (Circuit): The circuit to operate on.
 
-            data (Dict[str, Any]): Associated data for the pass.
+            data (PassData): Associated data for the pass.
                 Can be used to get auxillary information from previous
                 passes and to store information for future passes.
                 This function should never error based on what is in
@@ -62,174 +56,121 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         """
 
     @staticmethod
-    def get_model(
-        circuit: Circuit | UnitaryMatrix,
-        data: dict[str, Any],
-    ) -> MachineModel:
+    def get_model(_: Any, data: PassData) -> MachineModel:
         """
         Retrieve the machine model from the data dictionary.
 
-        Args:
-            circuit (Circuit): The pass circuit.
-
-            data (dict[str, Any]): The data dictionary.
-
-        Returns:
-            MachineModel: The machine model in the data dictionary, or
-                a default one.
+        (Deprecated)
         """
-
-        if len(data) == 0:
-            return MachineModel(circuit.num_qudits)
-
-        if 'machine_model' not in data:
-            data['machine_model'] = MachineModel(circuit.num_qudits)
-
-        if (
-            not isinstance(data['machine_model'], MachineModel)
-            or data['machine_model'].num_qudits < circuit.num_qudits
-        ):
-            _logger.warning('Expected machine_model to be a valid model.')
-            return MachineModel(circuit.num_qudits)
-
-        return data['machine_model']
+        warnings.warn(
+            'BasePass calls to retrieve elements from the data dictionary'
+            ' are now deprecated. We have upgraded the features of the data'
+            ' dictionary. It can now be used directly to retrieve elements'
+            " and provide defaults if they don't exist. This warning will"
+            ' become an error in the future.',
+            DeprecationWarning,
+        )
+        return data.model
 
     @staticmethod
-    def get_placement(
-        circuit: Circuit | UnitaryMatrix,
-        data: dict[str, Any],
-    ) -> list[int]:
+    def get_placement(_: Any, data: PassData) -> list[int]:
         """
         Retrieve the logical to physical qubit map from the data dictionary.
 
-        Args:
-            circuit (Circuit | UnitaryMatrix): The pass circuit.
-
-            data (dict[str, Any]): The data dictionary.
-
-        Returns:
-            (list[int]): A list with length equal to circuit.num_qudits.
-            Logical qubit i is mapped to the physical qubit described
-            by the i-th element in the list.
+        (Deprecated)
         """
-        # Default placement is trivial map: i -> i
-        default_placement = list(range(circuit.num_qudits))
-        model = BasePass.get_model(circuit, data)
-        sg = model.coupling_graph.get_subgraph(default_placement)
-
-        # Check in data for existing placement
-        if 'placement' in data:
-            p = data['placement']
-
-            # Check it's valid
-            if is_sequence(p) and len(p) == circuit.num_qudits:
-                return data['placement']
-
-        # If none found try to return default placement
-        if sg.is_fully_connected():
-            if len(data) != 0:
-                data['placement'] = default_placement
-            return default_placement
-
-        raise RuntimeError(
-            "No valid placement found and trivial one doesn't work."
-            '\nConsider running a placement pass before the current pass.',
+        warnings.warn(
+            'BasePass calls to retrieve elements from the data dictionary'
+            ' are now deprecated. We have upgraded the features of the data'
+            ' dictionary. It can now be used directly to retrieve elements'
+            " and provide defaults if they don't exist. This warning will"
+            ' become an error in the future.',
+            DeprecationWarning,
         )
+        return data.placement
 
     @staticmethod
-    def get_connectivity(
-        circuit: Circuit | UnitaryMatrix,
-        data: dict[str, Any],
-    ) -> CouplingGraph:
+    def get_connectivity(_: Any, data: PassData) -> CouplingGraph:
         """
         Retrieve the current connectivity of the circuit.
 
-        Args:
-            circuit (Circuit | UnitaryMatrix): The pass circuit.
-
-            data (dict[str, Any]): The data dictionary.
-
-        Returns:
-            (list[int]): A coupling graph of the connectivity.
+        (Deprecated)
         """
-        model = BasePass.get_model(circuit, data)
-        try:
-            placement = BasePass.get_placement(circuit, data)
-        except RuntimeError:
-            placement = list(range(circuit.num_qudits))
-        return model.coupling_graph.get_subgraph(placement)
+        warnings.warn(
+            'BasePass calls to retrieve elements from the data dictionary'
+            ' are now deprecated. We have upgraded the features of the data'
+            ' dictionary. It can now be used directly to retrieve elements'
+            " and provide defaults if they don't exist. This warning will"
+            ' become an error in the future.',
+            DeprecationWarning,
+        )
+        return data.connectivity
 
     @staticmethod
-    def get_target(circuit: Circuit, data: dict[str, Any]) -> UnitaryMatrix:
+    def get_target(
+        _: Any,
+        data: PassData,
+    ) -> UnitaryMatrix | StateVector | StateSystem:
         """
-        Retrieve the target unitary from the data dictionary.
+        Retrieve the target from the data dictionary.
 
-        Args:
-            circuit (Circuit): The pass circuit.
+        (Deprecated)
+        """
+        warnings.warn(
+            'BasePass calls to retrieve elements from the data dictionary'
+            ' are now deprecated. We have upgraded the features of the data'
+            ' dictionary. It can now be used directly to retrieve elements'
+            " and provide defaults if they don't exist. This warning will"
+            ' become an error in the future.',
+            DeprecationWarning,
+        )
+        return data.target
 
-            data (dict[str, Any]): The data dictionary.
+    @staticmethod
+    def in_parallel(data: dict[str, Any]) -> bool:
+        """
+        Return true if pass is being executed in a parallel.
 
-        Returns:
-            UnitaryMatrix: The target unitary.
+        (Deprecated)
         """
-        if len(data) == 0:
-            return circuit.get_unitary()
+        warnings.warn(
+            'BasePass calls to `in_parallel` are deprecated and will always'
+            ' return True. This warning will become an error in the future.',
+            DeprecationWarning,
+        )
+        return True
 
-        if 'target_unitary' not in data:
-            data['target_unitary'] = circuit.get_unitary()
+    @staticmethod
+    def execute(*args: Any, **kwargs: Any) -> Any:
+        """
+        Map a function over iterable arguments in parallel.
 
-        if not isinstance(data['target_unitary'], UnitaryMatrix):
-            _logger.warning('Expected target_unitary to be a unitary.')
-            return circuit.get_unitary()
+        (Defunct)
+        """
+        raise RuntimeError(
+            'Since Dask was removed, the execute function became defunct.'
+            ' We have switched to using `get_runtime().map(...)`. You can'
+            ' import `get_runtime` from `bqskit.runtime`, and alongside a'
+            ' an `await` keyword, should be a  drop-in replacement for the'
+            ' execute function. See the following link for more info: '
+            'https://bqskit.readthedocs.io/en/latest/source/runtime.html'
+            'In a future version, this error will become an AttributeError.',
+        )
 
-        return data['target_unitary']
 
-    @staticmethod
-    def in_parallel(data: dict[str, Any]) -> bool:
-        """Return true if pass is being executed in a parallel environment."""
-        if 'parallel' not in data:
-            return False
+async def _sub_do_work(
+    workflow: Workflow,
+    circuit: Circuit,
+    data: PassData,
+) -> tuple[Circuit, PassData]:
+    """Execute a sequence of passes on circuit."""
+    if 'calculate_error_bound' in data and data['calculate_error_bound']:
+        old_utry = circuit.get_unitary()
 
-        if not isinstance(data['parallel'], bool):
-            _logger.warning('Expected parallel to be a bool.')
-            return False
+    await workflow.run(circuit, data)
 
-        return data['parallel']
+    if 'calculate_error_bound' in data and data['calculate_error_bound']:
+        new_utry = circuit.get_unitary()
+        data.error = new_utry.get_distance_from(old_utry)
 
-    @staticmethod
-    def execute(
-        data: dict[str, Any],
-        fn: Callable[..., T],
-        *args: Any,
-        **kwargs: Any,
-    ) -> list[T]:
-        """Map a function over iterable arguments in parallel."""
-
-        if not all(is_iterable(arg) for arg in args):
-            raise ValueError('Each argument must be a sized iterable')
-
-        if not all(len(arg) == len(args[0]) for arg in args):
-            raise ValueError('Each iterable argument must have same length.')
-
-        # Execute using dask if dask is started and there are multiple tasks
-        if BasePass.in_parallel(data) and len(args[0]) > 1:
-            if fn == Circuit.instantiate:
-                kwargs['parallel'] = True
-
-            client = get_client()
-            futures = client.map(fn, *args, **kwargs)
-            secede()
-            results = client.gather(futures)
-            rejoin()
-            return results
-
-        # Otherwise execute in current process
-        else:
-            results = []
-            if len(args) == 1:
-                for arg in args[0]:
-                    results.append(fn(arg, **kwargs))
-            else:
-                for subargs in zip(*args):
-                    results.append(fn(*subargs, **kwargs))
-            return results
+    return circuit, data
diff --git a/bqskit/compiler/compile.py b/bqskit/compiler/compile.py
index 41679806e..ee9fcbe34 100644
--- a/bqskit/compiler/compile.py
+++ b/bqskit/compiler/compile.py
@@ -1,19 +1,22 @@
 """This module defines a standard `compile` function using BQSKit."""
 from __future__ import annotations
 
+import functools
 import logging
 import warnings
 from typing import Any
 from typing import Callable
+from typing import cast
 from typing import TYPE_CHECKING
 
 import numpy as np
 
-from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.compiler import Compiler
 from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
 from bqskit.compiler.task import CompilationTask
 from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
 from bqskit.ir.gates import CNOTGate
 from bqskit.ir.gates import RZGate
 from bqskit.ir.gates import SqrtXGate
@@ -21,26 +24,36 @@
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.gates.parameterized.u8 import U8Gate
+from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 from bqskit.ir.operation import Operation
 from bqskit.ir.opt import HilbertSchmidtCostGenerator
 from bqskit.ir.opt import ScipyMinimizer
+from bqskit.ir.opt.minimizers.lbfgs import LBFGSMinimizer
 from bqskit.passes import *
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
+from bqskit.qis.state.system import StateSystemLike
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_real_number
-_logger = logging.getLogger(__name__)
+
+
 if TYPE_CHECKING:
     from bqskit.qis.unitary import UnitaryLike
     from bqskit.qis.state import StateLike
+    from bqskit.compiler.basepass import BasePass
+
+
+_logger = logging.getLogger(__name__)
 
 
 def compile(
-    input: Circuit | UnitaryLike | StateLike,
+    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
     model: MachineModel | None = None,
     optimization_level: int = 1,
     max_synthesis_size: int = 3,
-    synthesis_epsilon: float = 1e-10,
+    synthesis_epsilon: float = 1e-8,
     error_threshold: float | None = None,
     error_sim_size: int = 8,
     compiler: Compiler | None = None,
@@ -91,11 +104,11 @@ def compile(
             when calling `compile` multiple times. (Default: None)
 
         compiler_args (Any): Passed directly to BQSKit compiler construction.
-            Arguments for connecting to a dask cluster can go here.
+            Arguments for connecting to a cluster can go here.
             See :class:`Compiler` for more info.
 
         compiler_kwargs (Any): Passed directly to BQSKit compiler construction.
-            Arguments for connecting to a dask cluster can go here.
+            Arguments for connecting to a cluster can go here.
             See :class:`Compiler` for more info.
 
     Returns:
@@ -118,10 +131,13 @@ def compile(
         elif StateVector.is_pure_state(input):
             input = StateVector(input)
 
+        elif StateSystem.is_state_system(input):
+            input = StateSystem(input)
+
         else:
             raise TypeError(
-                'Input is neither a circuit, a unitary, nor a state.'
-                f' Got {type(input)}.',
+                'Input is neither a circuit, a unitary, a state system'
+                f', nor a state. Got {type(input)}.',
             )
     except Exception as e:
         raise TypeError(
@@ -130,17 +146,19 @@ def compile(
             ' circuit, unitary, or state.',
         ) from e
 
-    assert isinstance(input, (Circuit, UnitaryMatrix, StateVector))
+    assert isinstance(input, (Circuit, UnitaryMatrix, StateVector, StateSystem))
 
-    if not all(r == 2 for r in input.radixes):
+    if not all(r == input.radixes[0] for r in input.radixes):
         raise ValueError(
             'Currently, can only automatically build a workflow '
-            'for qubit-only systems.',
-        )  # TODO
+            'for same-level systems, such as qubit-only or qutrit-only'
+            'systems. Heterogenous-radix systems are not yet supported'
+            'with the standard workflows.',
+        )
 
     # Check `model`
     if model is None:
-        model = MachineModel(input.num_qudits)
+        model = MachineModel(input.num_qudits, radixes=input.radixes)
 
     if not isinstance(model, MachineModel):
         raise TypeError(f'Expected MachineModel for model, got {type(model)}.')
@@ -148,11 +166,13 @@ def compile(
     if model.num_qudits < input.num_qudits:
         raise ValueError('Machine is too small for circuit.')
 
-    if not all(r == 2 for r in model.radixes):
+    if not all(r == input.radixes[0] for r in model.radixes):
         raise ValueError(
             'Currently, can only automatically build a workflow '
-            'for qubit-only systems.',
-        )  # TODO
+            'for same-level systems, such as qubit-only or qutrit-only'
+            'systems. Heterogenous-radix systems are not yet supported'
+            'with the standard workflows.',
+        )
 
     model_mq_gates = [g for g in model.gate_set if g.num_qudits >= 2]
 
@@ -304,6 +324,25 @@ def compile(
             error_sim_size,
         )
 
+    elif isinstance(input, StateSystem):
+        statemap = input
+
+        if statemap.num_qudits > max_synthesis_size:
+            raise ValueError(
+                'Cannot synthesize state larger than max_synthesis_size.\n'
+                'Consider adjusting max_synthesis_size or checking the input.',
+            )
+
+        task = _statemap_workflow(
+            statemap,
+            model,
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        )
+
     # Connect to or construct a Compiler
     managed_compiler = compiler is None
 
@@ -317,11 +356,18 @@ def compile(
         )
 
     # Perform the compilation
-    out = compiler.compile(task)
+    if error_threshold is None:
+        t_id = compiler.submit(task)
+        out = compiler.result(t_id)
+    else:
+        task.request_data = True
+        t_id = compiler.submit(task)
+        out, data = compiler.result(t_id)  # type: ignore
 
     # Log error if necessary
     if error_threshold is not None:
-        error = compiler.analyze(task, 'error')
+        data = cast(PassData, data)
+        error = data.error
         nonsq_error = 1 - np.sqrt(max(1 - (error * error), 0))
         if nonsq_error > error_threshold:
             warnings.warn(
@@ -333,7 +379,7 @@ def compile(
     if managed_compiler:
         compiler.close()
 
-    return out
+    return cast(Circuit, out)
 
 
 def _circuit_workflow(
@@ -404,19 +450,26 @@ def _opt1_workflow(
             g for g in non_native_gates
             if g.num_qudits == 2
         ]
-        if SwapGate() not in model.gate_set:
-            non_native_tq_gates.append(SwapGate())
+        if SwapGate(model.radixes[0]) not in model.gate_set:
+            non_native_tq_gates.append(SwapGate(model.radixes[0]))
         native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
 
         all_gates = model.gate_set.union(circuit.gate_set)
         if any(g.num_qudits > 2 for g in all_gates):
             multi_qudit_gate_rebase: BasePass = direct_synthesis
         else:
+            if model.radixes[0] == 2:
+                sq_gate: Gate = U3Gate()
+            elif model.radixes[0] == 3:
+                sq_gate = U8Gate()
+            else:
+                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
             multi_qudit_gate_rebase = Rebase2QuditGatePass(
                 non_native_tq_gates,
                 native_tq_gates,
                 max_depth=3,
                 max_retries=5,
+                single_qudit_gate=sq_gate,
             )
     else:
         smallest_entangler_size = 1
@@ -534,19 +587,26 @@ def _opt2_workflow(
             g for g in non_native_gates
             if g.num_qudits == 2
         ]
-        if SwapGate() not in model.gate_set:
-            non_native_tq_gates.append(SwapGate())
+        if SwapGate(model.radixes[0]) not in model.gate_set:
+            non_native_tq_gates.append(SwapGate(model.radixes[0]))
         native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
 
         all_gates = model.gate_set.union(circuit.gate_set)
         if any(g.num_qudits > 2 for g in all_gates):
             multi_qudit_gate_rebase: BasePass = direct_synthesis
         else:
+            if model.radixes[0] == 2:
+                sq_gate: Gate = U3Gate()
+            elif model.radixes[0] == 3:
+                sq_gate = U8Gate()
+            else:
+                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
             multi_qudit_gate_rebase = Rebase2QuditGatePass(
                 non_native_tq_gates,
                 native_tq_gates,
                 max_depth=3,
                 max_retries=5,
+                single_qudit_gate=sq_gate,
             )
     else:
         smallest_entangler_size = 1
@@ -680,8 +740,8 @@ def _opt3_workflow(
             g for g in non_native_gates
             if g.num_qudits == 2
         ]
-        if SwapGate() not in model.gate_set:
-            non_native_tq_gates.append(SwapGate())
+        if SwapGate(model.radixes[0]) not in model.gate_set:
+            non_native_tq_gates.append(SwapGate(model.radixes[0]))
         native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
         native_mq_gates = [g for g in model.gate_set if g.num_qudits >= 2]
 
@@ -689,11 +749,18 @@ def _opt3_workflow(
         if any(g.num_qudits > 2 for g in all_gates):
             multi_qudit_gate_rebase: BasePass = direct_synthesis
         else:
+            if model.radixes[0] == 2:
+                sq_gate: Gate = U3Gate()
+            elif model.radixes[0] == 3:
+                sq_gate = U8Gate()
+            else:
+                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
             multi_qudit_gate_rebase = Rebase2QuditGatePass(
                 non_native_tq_gates,
                 native_tq_gates,
                 max_depth=3,
                 max_retries=5,
+                single_qudit_gate=sq_gate,
             )
     else:
         smallest_entangler_size = 1
@@ -832,12 +899,176 @@ def _stateprep_workflow(
     error_sim_size: int = 8,
 ) -> CompilationTask:
     """Build a workflow for state preparation."""
-    # TODO
-    raise NotImplementedError('State preparation is not yet implemented.')
+    circuit = Circuit(1)
+    layer_gen = _get_layer_gen(model)
+
+    if optimization_level == 1:
+        inst_ops = {
+            'multistarts': 1,
+            'method': 'minimization',
+            'minimizer': LBFGSMinimizer(),
+        }
+        synthesis = LEAPSynthesisPass(
+            success_threshold=synthesis_epsilon,
+            layer_generator=layer_gen,
+            instantiate_options=inst_ops,
+            min_prefix_size=3,
+            cost=HilbertSchmidtCostGenerator(),
+        )
+
+    elif optimization_level == 2:
+        inst_ops = {
+            'multistarts': 4,
+            'method': 'minimization',
+            'ftol': 5e-12,
+            'gtol': 1e-14,
+        }
+        synthesis = LEAPSynthesisPass(
+            success_threshold=synthesis_epsilon,
+            layer_generator=layer_gen,
+            instantiate_options=inst_ops,
+            min_prefix_size=5,
+        )
+
+    elif optimization_level == 3:
+        inst_ops = {
+            'multistarts': 8,
+            'method': 'minimization',
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
+        if circuit.num_qudits > 3:
+            synthesis = LEAPSynthesisPass(
+                success_threshold=synthesis_epsilon,
+                layer_generator=layer_gen,
+                instantiate_options=inst_ops,
+                min_prefix_size=7,
+            )
+        else:
+            synthesis = QSearchSynthesisPass(  # type: ignore
+                success_threshold=synthesis_epsilon,
+                layer_generator=layer_gen,
+                instantiate_options=inst_ops,
+            )
+
+    elif optimization_level == 4:
+        inst_ops = {
+            'multistarts': 8,
+            'method': 'minimization',
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
+        raise NotImplementedError('Not yet.')
+
+    scan = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        instantiate_options=inst_ops,
+        cost=HilbertSchmidtCostGenerator(),
+    )
+
+    workflow = [
+        SetModelPass(model),
+        SetTargetPass(state),
+        synthesis,
+        scan,
+    ]
+
+    return CompilationTask(circuit, workflow)
+
+
+def _statemap_workflow(
+    state: StateSystem,
+    model: MachineModel,
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-8,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> CompilationTask:
+    """Build a workflow for state preparation."""
+    circuit = Circuit(1)
+    layer_gen = _get_layer_gen(model)
+
+    if optimization_level == 1:
+        inst_ops = {
+            'multistarts': 1,
+            'method': 'minimization',
+        }
+        synthesis = LEAPSynthesisPass(
+            success_threshold=synthesis_epsilon,
+            layer_generator=layer_gen,
+            instantiate_options=inst_ops,
+            min_prefix_size=3,
+        )
+
+    elif optimization_level == 2:
+        inst_ops = {
+            'multistarts': 4,
+            'method': 'minimization',
+            'ftol': 5e-12,
+            'gtol': 1e-14,
+        }
+        synthesis = LEAPSynthesisPass(
+            success_threshold=synthesis_epsilon,
+            layer_generator=layer_gen,
+            instantiate_options=inst_ops,
+            min_prefix_size=5,
+        )
+
+    elif optimization_level == 3:
+        inst_ops = {
+            'multistarts': 8,
+            'method': 'minimization',
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
+        if circuit.num_qudits > 3:
+            synthesis = LEAPSynthesisPass(
+                success_threshold=synthesis_epsilon,
+                layer_generator=layer_gen,
+                instantiate_options=inst_ops,
+                min_prefix_size=7,
+            )
+        else:
+            synthesis = QSearchSynthesisPass(  # type: ignore
+                success_threshold=synthesis_epsilon,
+                layer_generator=layer_gen,
+                instantiate_options=inst_ops,
+            )
+
+    elif optimization_level == 4:
+        inst_ops = {
+            'multistarts': 8,
+            'method': 'minimization',
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
+        raise NotImplementedError('Not yet.')
+
+    scan = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        instantiate_options=inst_ops,
+    )
+
+    workflow = [
+        SetModelPass(model),
+        SetTargetPass(state),
+        synthesis,
+        scan,
+    ]
+
+    return CompilationTask(circuit, workflow)
 
 
 def _get_layer_gen(model: MachineModel) -> LayerGenerator:
     """Build a `model`-compliant layer generator."""
+    if model.radixes[0] == 2:
+        sq_gate: Gate = U3Gate()
+    elif model.radixes[0] == 3:
+        sq_gate = U8Gate()
+    else:
+        sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
+
     tq_gates = [gate for gate in model.gate_set if gate.num_qudits == 2]
     mq_gates = [gate for gate in model.gate_set if gate.num_qudits > 2]
 
@@ -845,9 +1076,9 @@ def _get_layer_gen(model: MachineModel) -> LayerGenerator:
         if CNOTGate() in tq_gates:
             return FourParamGenerator()
         else:
-            return SimpleLayerGenerator(tq_gates[0])
+            return SimpleLayerGenerator(tq_gates[0], sq_gate)
 
-    return WideLayerGenerator(tq_gates + mq_gates)
+    return WideLayerGenerator(tq_gates + mq_gates, sq_gate)
 
 
 def _get_single_qudit_gate_rebase_pass(model: MachineModel) -> BasePass:
@@ -904,34 +1135,36 @@ def _get_single_qudit_gate_rebase_pass(model: MachineModel) -> BasePass:
     ])
 
 
+def _replace_filter(new: Circuit, old: Operation, model: MachineModel) -> bool:
+    # return true if old doesn't satisfy model
+    if not isinstance(old.gate, CircuitGate):
+        return True
+
+    org = old.gate._circuit
+    org_mq_gates = [g for g in org.gate_set if g.num_qudits > 1]
+
+    if any(g not in model.gate_set for g in org_mq_gates):
+        return True
+
+    if any(
+        (old.location[e[0]], old.location[e[1]]) not in model.coupling_graph
+        for e in org.coupling_graph
+    ):
+        return True
+
+    # else pick shortest circuit
+    org_sq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits == 1)
+    org_mq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits >= 2)
+    new_sq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits == 1)
+    new_mq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits >= 2)
+    return (new_mq_n, new_sq_n) < (org_mq_n, org_sq_n)
+
+
 def _gen_replace_filter(
     model: MachineModel,
 ) -> Callable[[Circuit, Operation], bool]:
     """Generate a replace filter for use during the standard workflow."""
-    def _replace_filter(new: Circuit, old: Operation) -> bool:
-        # return true if old doesn't satisfy model
-        if not isinstance(old.gate, CircuitGate):
-            return True
-
-        org = old.gate._circuit
-        org_mq_gates = [g for g in org.gate_set if g.num_qudits > 1]
-
-        if any(g not in model.gate_set for g in org_mq_gates):
-            return True
-
-        if any(
-            (old.location[e[0]], old.location[e[1]]) not in model.coupling_graph
-            for e in org.coupling_graph
-        ):
-            return True
-
-        # else pick shortest circuit
-        org_sq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits == 1)
-        org_mq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits >= 2)
-        new_sq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits == 1)
-        new_mq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits >= 2)
-        return (new_mq_n, new_sq_n) < (org_mq_n, org_sq_n)
-    return _replace_filter
+    return functools.partial(_replace_filter, model=model)
 
 
 def _mq_gate_collection_filter(op: Operation) -> bool:
diff --git a/bqskit/compiler/compiler.py b/bqskit/compiler/compiler.py
index 560f90871..974c1a193 100644
--- a/bqskit/compiler/compiler.py
+++ b/bqskit/compiler/compiler.py
@@ -1,25 +1,35 @@
 """This module implements the Compiler class."""
 from __future__ import annotations
 
+import atexit
+import functools
 import logging
+import multiprocessing as mp
+import os
+import signal
 import sys
 import time
 import uuid
-from subprocess import PIPE
-from subprocess import Popen
+import warnings
+from multiprocessing.connection import Client
+from multiprocessing.connection import Connection
+from types import FrameType
+from typing import overload
 from typing import TYPE_CHECKING
 
+from bqskit.compiler.passdata import PassData
+from bqskit.compiler.status import CompilationStatus
+from bqskit.compiler.task import CompilationTask
+from bqskit.compiler.workflow import Workflow
+from bqskit.compiler.workflow import WorkflowLike
+from bqskit.runtime import default_server_port
+from bqskit.runtime import default_worker_port
+from bqskit.runtime.message import RuntimeMessage
+
 if TYPE_CHECKING:
     from typing import Any
-    from dask.distributed import Future
-    from bqskit.compiler.task import CompilationTask
     from bqskit.ir.circuit import Circuit
 
-from dask.distributed import Client
-from threadpoolctl import threadpool_limits
-
-from bqskit.compiler.executor import Executor
-
 _logger = logging.getLogger(__name__)
 
 
@@ -27,11 +37,12 @@ class Compiler:
     """
     A compiler is responsible for accepting and managing compilation tasks.
 
-    The compiler class spins up or connects to a Dask execution environment,
-    which compilation tasks can then access to parallelize their operations.
-    The compiler is implemented as a context manager and it is recommended
-    to use it as one. If the compiler is not used in a context manager, it
-    is the responsibility of the user to call `close()`.
+    The compiler class either spins up a parallel runtime or connects to
+    a distributed one, which compilation tasks can then access to
+    parallelize their operations. The compiler is implemented as a
+    context manager and it is recommended to use it as one. If the
+    compiler is not used in a context manager, it is the responsibility
+    of the user to call `close()`.
 
     Examples:
         1. Use in a context manager:
@@ -43,52 +54,69 @@ class Compiler:
         >>> circuit = compiler.compile(task)
         >>> compiler.close()
 
-        3. Connect to an already running dask cluster:
-        >>> with Compiler('localhost:8786') as compiler:
+        3. Connect to an already running detached runtime:
+        >>> with Compiler('localhost', 8786) as compiler:
         ...     circuit = compiler.compile(task)
 
-        4. Connect to a dask cluster with a scheduler file:
-        >>> with Compiler(scheduler_file='/path/to/file/') as compiler:
+        4. Start and attach to a runtime with 4 worker processes:
+        >>> with Compiler(num_workers=4) as compiler:
         ...     circuit = compiler.compile(task)
     """
 
-    _parallelism_enabled = True
-    """Used to to limit default parallelism; see bqskit.disable_parallelism."""
-
-    _dashboard = False
-    """Used to enable the dask dashboard; see bqskit.enable_dashboard."""
-
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
-        """
-        Construct a Compiler object.
-
-        Notes:
-            All arguments are passed directly to Dask. You can use
-            these to connect to and configure a Dask cluster.
-        """
-        # Connect to or start a Dask cluster
-        self.managed = len(args) == 0 and len(kwargs) == 0
-        if self.managed:
-            self.processes = start_dask_cluster()
-            self.client = Client('localhost:8786')
-        else:
-            self.client = Client(*args, **kwargs)
-
-        # Initialize workers for logging support
-        def set_logging_level(level: int) -> None:
-            import bqskit  # noqa
-            logging.getLogger('bqskit').setLevel(level)
-
-        self.client.run(set_logging_level, logging.getLogger('bqskit').level)
-
-        # Limit threads for low level math calls on workers
-        def limit_threads() -> None:
-            threadpool_limits(limits=1)
-        self.client.run(limit_threads)
-
-        # Keep track of submitted compilation tasks
-        self.tasks: dict[uuid.UUID, Future] = {}
-        _logger.info('Started compiler process.')
+    def __init__(
+        self,
+        ip: None | str = None,
+        port: int = default_server_port,
+        num_workers: int = -1,
+        runtime_log_level: int = logging.WARNING,
+        worker_port: int = default_worker_port,
+    ) -> None:
+        """Construct a Compiler object."""
+        self.p: mp.Process | None = None
+        self.conn: Connection | None = None
+
+        atexit.register(self.close)
+        if ip is None:
+            ip = 'localhost'
+            self._start_server(num_workers, runtime_log_level, worker_port)
+
+        self._connect_to_server(ip, port)
+
+    def _start_server(
+        self,
+        num_workers: int,
+        runtime_log_level: int,
+        worker_port: int,
+    ) -> None:
+        from bqskit.runtime.attached import start_attached_server
+        self.p = mp.Process(
+            target=start_attached_server,
+            args=(num_workers, runtime_log_level),
+            kwargs={'worker_port': worker_port},
+        )
+        _logger.debug('Starting runtime server process.')
+        self.p.start()
+
+    def _connect_to_server(self, ip: str, port: int) -> None:
+        max_retries = 7
+        wait_time = .25
+        for _ in range(max_retries):
+            try:
+                family = 'AF_INET' if sys.platform == 'win32' else None
+                conn = Client((ip, port), family)
+            except ConnectionRefusedError:
+                time.sleep(wait_time)
+                wait_time *= 2
+            else:
+                self.conn = conn
+                handle = functools.partial(sigint_handler, compiler=self)
+                self.old_signal = signal.signal(signal.SIGINT, handle)
+                if self.conn is None:
+                    raise RuntimeError('Connection unexpectedly none.')
+                self.conn.send((RuntimeMessage.CONNECT, None))
+                _logger.debug('Successfully connected to runtime server.')
+                return
+        raise RuntimeError('Client connection refused')
 
     def __enter__(self) -> Compiler:
         """Enter a context for this compiler."""
@@ -99,108 +127,346 @@ def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
         self.close()
 
     def close(self) -> None:
-        """Shutdown compiler."""
-        self.client.close()
-        self.tasks = {}
-        if self.managed:
-            for proc in self.processes:
-                proc.terminate()  # type: ignore
-                proc.wait()  # type: ignore
-        _logger.info('Stopped compiler process.')
+        """Shutdown the compiler."""
+        # Disconnect from server
+        if self.conn is not None:
+            try:
+                self.conn.send((RuntimeMessage.DISCONNECT, None))
+                try:
+                    self.conn.recv()
+                except EOFError:
+                    # When the server disconnects the conn, we then proceed
+                    pass
+                self.conn.close()
+            except Exception as e:
+                _logger.debug(
+                    'Unsuccessfully disconnected from runtime server.',
+                )
+                _logger.debug(e)
+            else:
+                _logger.debug('Disconnected from runtime server.')
+            finally:
+                self.conn = None
+
+        # Shutdown server if attached
+        if self.p is not None and self.p.pid is not None:
+            try:
+                os.kill(self.p.pid, signal.SIGINT)
+                _logger.debug('Interrupted attached runtime server.')
+
+                self.p.join(1)
+                if self.p.exitcode is None:
+                    if sys.platform == 'win32':
+                        self.p.terminate()
+                    else:
+                        os.kill(self.p.pid, signal.SIGKILL)
+                    _logger.debug('Killed attached runtime server.')
+
+            except Exception as e:
+                _logger.debug(
+                    f'Error while shuting down attached runtime server: {e}.',
+                )
+            else:
+                _logger.debug('Successfully shutdown attached runtime server.')
+            finally:
+                self.p.join()
+                _logger.debug('Attached runtime server is down.')
+                self.p = None
+
+        # Reset interrupt signal handler and remove exit handler
+        if hasattr(self, 'old_signal'):
+            signal.signal(signal.SIGINT, self.old_signal)
 
     def __del__(self) -> None:
-        if self.managed and hasattr(self, 'processes'):
-            for proc in self.processes:
-                proc.terminate()  # type: ignore
-                proc.wait()  # type: ignore
-
-    def submit(self, task: CompilationTask) -> None:
-        """Submit a CompilationTask to the Compiler."""
-        executor = self.client.scatter(Executor(task))
-        future = self.client.submit(Executor.run, executor, pure=False)
-        self.tasks[task.task_id] = future
-        _logger.info('Submitted task: %s' % task.task_id)
-
-    def status(self, task: CompilationTask) -> str:
-        """Retrieve the status of the specified CompilationTask."""
-        return self.tasks[task.task_id].status
-
-    def result(self, task: CompilationTask) -> Circuit:
-        """Block until the CompilationTask is finished, return its result."""
-        circ = self.tasks[task.task_id].result()[0]
-        return circ
-
-    def cancel(self, task: CompilationTask) -> None:
-        """Remove a task from the compiler's workqueue."""
-        self.client.cancel(self.tasks[task.task_id])
-        _logger.info('Cancelled task: %s' % task.task_id)
-
-    def compile(self, task: CompilationTask) -> Circuit:
-        """Submit and execute the CompilationTask, block until its done."""
-        _logger.info('Compiling task: %s' % task.task_id)
-        self.submit(task)
-        result = self.result(task)
-        return result
+        self.close()
+        atexit.unregister(self.close)
+        _logger.debug('Compiler successfully shutdown.')
+
+    def submit(
+        self,
+        task_or_circuit: CompilationTask | Circuit,
+        workflow: WorkflowLike | None = None,
+        request_data: bool = False,
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> uuid.UUID:
+        """
+        Submit a compilation job to the Compiler.
+
+        Args:
+            task_or_circuit (CompilationTask | Circuit): The task to compile,
+                or the input circuit. If a task is specified, no other
+                argument should be specified. If a task is not specified,
+                the circuit must be paired with a workflow argument.
+
+            workflow (WorkflowLike): The compilation job submitted
+                is defined by executing this workflow on the input circuit.
+
+            request_data (bool): If true, the task result will contain the
+                associated pass data accumulated during compilation.
+                Defaults to False.
+
+            logging_level (int | None): Specify the python logging level
+                to be used during compilation. Defaults to None, which
+                will use current logging configuration.
+
+            max_logging_depth (int): Compilation jobs will create subtasks
+                which may also create subtasks. Tasks that have
+                `max_logging_depth` ancestors or more will stop logging.
+                Defaults to -1, which disables the feature, allowing all
+                tasks equal opportunity to log.
+
+        Returns:
+            (uuid.UUID): The ID of the generated task in the system. This
+                ID can be used to check the status of, cancel, and request
+                the result of the task.
+        """
+        # Build CompilationTask
+        if isinstance(task_or_circuit, CompilationTask):
+            if workflow is not None:
+                raise ValueError(
+                    'Cannot specify workflow and task.'
+                    ' Either specify a workflow and circuit or a task alone.',
+                )
+
+            task = task_or_circuit
 
-    def analyze(self, task: CompilationTask, key: str) -> Any:
-        """Gather the value associated with `key` in the task's data."""
-        if task.task_id not in self.tasks:
-            self.submit(task)
-        return self.tasks[task.task_id].result()[1][key]
-
-
-def start_dask_cluster() -> tuple[Popen[bytes], Popen[str]]:
-    """Start a dask cluster and return the scheduler and worker processes."""
-    scheduler_proc: Any = None
-    worker_proc: Any = None
-
-    # Start a dask scheduler in another process
-    try:
-        dashboard = '--dashboard' if Compiler._dashboard else '--no-dashboard'
-        scheduler_proc = Popen(
-            [
-                'dask-scheduler',
-                '--host', 'localhost',
-                '--port', '8786',
-                dashboard,
-            ],
-            bufsize=1,
-            universal_newlines=True,
-            stdout=sys.stdout,
-            stderr=PIPE,
-        )
-        for line in iter(scheduler_proc.stderr.readline, ''):
-            if 'Scheduler at:' in line:
-                break
-        scheduler_proc.stderr.close()
-    except Exception as e:
-        raise RuntimeError('Failed to start dask scheduler') from e
-
-    # Start dask workers in another process
-    try:
-        workers = []
-        if Compiler._parallelism_enabled:
-            workers.append('auto')
         else:
-            workers.append('1')
-            workers.append('--nthreads')
-            workers.append('1')
-
-        worker_proc = Popen(
-            ['dask-worker', '--nworkers', *workers, 'localhost:8786'],
-            bufsize=1,
-            universal_newlines=True,
-            stdout=sys.stdout,
-            stderr=PIPE,
+            if workflow is None:
+                m = 'Must specify workflow when providing a circuit to submit.'
+                raise TypeError(m)
+
+            task = CompilationTask(task_or_circuit, Workflow(workflow))
+
+        # Set task configuration
+        task.request_data = request_data
+        task.logging_level = logging_level or self._discover_lowest_log_level()
+        task.max_logging_depth = max_logging_depth
+
+        # Submit task to runtime
+        self._send(RuntimeMessage.SUBMIT, task)
+        return task.task_id
+
+    def status(self, task_id: CompilationTask | uuid.UUID) -> CompilationStatus:
+        """Retrieve the status of the specified task."""
+        if isinstance(task_id, CompilationTask):
+            warnings.warn(
+                'Request a status from a CompilationTask is deprecated.'
+                ' Instead, pass a task ID to request a status.'
+                'This warning will turn into an error in a future update.',
+                DeprecationWarning,
+            )
+            task_id = task_id.task_id
+        assert isinstance(task_id, uuid.UUID)
+
+        msg, payload = self._send_recv(RuntimeMessage.STATUS, task_id)
+        if msg != RuntimeMessage.STATUS:
+            raise RuntimeError(f'Unexpected message type: {msg}.')
+        return payload
+
+    def result(
+        self,
+        task_id: CompilationTask | uuid.UUID,
+    ) -> Circuit | tuple[Circuit, PassData]:
+        """Block until the task is finished, return its result."""
+        if isinstance(task_id, CompilationTask):
+            warnings.warn(
+                'Request a result from a CompilationTask is deprecated.'
+                ' Instead, pass a task ID to request a result.'
+                'This warning will turn into an error in a future update.',
+                DeprecationWarning,
+            )
+            task_id = task_id.task_id
+        assert isinstance(task_id, uuid.UUID)
+
+        msg, payload = self._send_recv(RuntimeMessage.REQUEST, task_id)
+        if msg != RuntimeMessage.RESULT:
+            raise RuntimeError(f'Unexpected message type: {msg}.')
+        return payload
+
+    def cancel(self, task_id: CompilationTask | uuid.UUID) -> bool:
+        """Cancel the execution of a task in the system."""
+        if isinstance(task_id, CompilationTask):
+            warnings.warn(
+                'Cancelling a CompilationTask is deprecated. Instead,'
+                ' cancel a task by passing its ID from `task.task_id`.'
+                'This warning will turn into an error in a future update.',
+                DeprecationWarning,
+            )
+            task_id = task_id.task_id
+        assert isinstance(task_id, uuid.UUID)
+
+        msg, _ = self._send_recv(RuntimeMessage.CANCEL, task_id)
+        if msg != RuntimeMessage.CANCEL:
+            raise RuntimeError(f'Unexpected message type: {msg}.')
+        return True
+
+    @overload
+    def compile(
+        self,
+        task_or_circuit: CompilationTask,
+    ) -> Circuit | tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
+        ...
+
+    @overload
+    def compile(
+        self,
+        task_or_circuit: Circuit,
+        workflow: WorkflowLike,
+        request_data: None = None,
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> Circuit:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
+        ...
+
+    @overload
+    def compile(
+        self,
+        task_or_circuit: Circuit,
+        workflow: WorkflowLike,
+        request_data: bool,
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
+        ...
+
+    def compile(
+        self,
+        task_or_circuit: CompilationTask | Circuit,
+        workflow: WorkflowLike | None = None,
+        request_data: bool | None = None,
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> Circuit | tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
+        if isinstance(task_or_circuit, CompilationTask):
+            warnings.warn(
+                'Manually constructing and compiling CompilationTasks'
+                ' is deprecated. Instead, call compile directly with'
+                ' your input circuit and workflow. This warning will'
+                ' turn into an error in a future update.',
+                DeprecationWarning,
+            )
+
+        task_id = self.submit(
+            task_or_circuit,
+            workflow,
+            request_data if request_data else False,
+            logging_level,
+            max_logging_depth,
         )
-        for line in iter(worker_proc.stderr.readline, ''):
-            if 'Starting established connection' in line:
-                break
-        worker_proc.stderr.close()
-    except Exception as e:
-        scheduler_proc.terminate()
-        scheduler_proc.wait()
-        raise RuntimeError('Failed to start dask worker') from e
-
-    time.sleep(1)
-    return worker_proc, scheduler_proc
+        result = self.result(task_id)
+
+        # Ensure arrival of all log messages
+        time.sleep(0.05 if self.p is not None else 0.5)
+        self._recv_log_error_until_empty()
+
+        return result
+
+    def _send(self, msg: RuntimeMessage, payload: Any) -> None:
+        """Send a message to the runtime."""
+        if self.conn is None:
+            raise RuntimeError('Connection unexpectedly none.')
+
+        try:
+            self._recv_log_error_until_empty()
+
+            self.conn.send((msg, payload))
+
+        except Exception as e:
+            self.conn = None
+            self.close()
+            raise RuntimeError('Server connection unexpectedly closed.') from e
+
+    def _send_recv(
+        self,
+        msg: RuntimeMessage,
+        payload: Any,
+    ) -> tuple[RuntimeMessage, Any]:
+        """Send a message to the runtime, and wait for a response."""
+        if self.conn is None:
+            raise RuntimeError('Connection unexpectedly none.')
+
+        try:
+            self._recv_log_error_until_empty()
+
+            self.conn.send((msg, payload))
+
+            return self._recv_handle_log_error()
+
+        except Exception as e:
+            self.conn = None
+            self.close()
+            raise RuntimeError('Server connection unexpectedly closed.') from e
+
+    def _recv_handle_log_error(self) -> tuple[RuntimeMessage, Any]:
+        """Return next msg, transparently emit log records and raise errors."""
+        if self.conn is None:
+            raise RuntimeError('Connection unexpectedly none.')
+
+        to_return = None
+        while to_return is None or self.conn.poll():
+            msg, payload = self.conn.recv()
+
+            if msg == RuntimeMessage.LOG:
+                logger = logging.getLogger(payload.name)
+                if logger.isEnabledFor(payload.levelno):
+                    logger.handle(payload)
+
+            elif msg == RuntimeMessage.ERROR:
+                raise RuntimeError(payload)
+
+            else:
+                # Communication between runtime server and compiler
+                # is always round-trip. Once we have received our
+                # desired message (not log or error) we can therefore be
+                # certain any remaining messages in the pipeline are
+                # only either logs or error messages. We do want to
+                # handle these sooner rather than later, so we ensure to
+                # process every arrived message before returning.
+                # Hence, the `or self.conn.poll()` in the while condition.
+                to_return = (msg, payload)
+
+        return to_return
+
+    def _recv_log_error_until_empty(self) -> None:
+        """Handle all remaining log and error messages in the pipeline."""
+        if self.conn is None:
+            raise RuntimeError('Connection unexpectedly none.')
+
+        while self.conn.poll():
+            msg, payload = self.conn.recv()
+
+            if msg == RuntimeMessage.LOG:
+                logger = logging.getLogger(payload.name)
+                if logger.isEnabledFor(payload.levelno):
+                    logger.handle(payload)
+
+            elif msg == RuntimeMessage.ERROR:
+                raise RuntimeError(payload)
+
+            else:
+                raise RuntimeError(f'Unexpected message type: {msg}.')
+
+    def _discover_lowest_log_level(self) -> int:
+        """Searches through all python loggers for the lowest set level."""
+        lowest_level_found_so_far = logging.getLogger().getEffectiveLevel()
+
+        for _, logger in logging.getLogger().manager.loggerDict.items():
+            if isinstance(logger, logging.PlaceHolder):
+                continue
+
+            if logger.getEffectiveLevel() < lowest_level_found_so_far:
+                lowest_level_found_so_far = logger.getEffectiveLevel()
+
+        return lowest_level_found_so_far
+
+
+def sigint_handler(signum: int, frame: FrameType, compiler: Compiler) -> None:
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+    _logger.critical('Compiler interrupted.')
+    compiler.close()
+    raise KeyboardInterrupt
diff --git a/bqskit/compiler/executor.py b/bqskit/compiler/executor.py
deleted file mode 100644
index 7e726bffb..000000000
--- a/bqskit/compiler/executor.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""This module implements the Executor class."""
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-from bqskit.compiler.task import CompilationTask
-if TYPE_CHECKING:
-    from typing import Any
-    from bqskit.ir.circuit import Circuit
-
-
-class Executor:
-    """An Executor is responsible for executing a CompilationTask."""
-
-    def __init__(self, task: CompilationTask) -> None:
-        """
-        Construct an Executor.
-
-        Creates a executor ready to execute the specified task.
-
-        Args:
-            task (CompilationTask): The task to execute.
-        """
-
-        if not isinstance(task, CompilationTask):
-            raise TypeError(f'Expected a CompilationTask, got {type(task)}.')
-
-        self.task_id = task.task_id
-        self.circuit = task.input_circuit
-        self.passes = task.passes
-        self.data: dict[str, Any] = {'parallel': True}
-        self.done = False
-
-    def run(self) -> tuple[Circuit, dict[str, Any]]:
-        """Execute the task."""
-        for pass_obj in self.passes:
-            pass_obj.run(self.circuit, self.data)
-        self.done = True
-        return self.circuit, self.data
diff --git a/bqskit/compiler/machine.py b/bqskit/compiler/machine.py
index 7bf84ef75..c97c4529a 100644
--- a/bqskit/compiler/machine.py
+++ b/bqskit/compiler/machine.py
@@ -2,22 +2,32 @@
 from __future__ import annotations
 
 from typing import Sequence
+from typing import TYPE_CHECKING
 
-from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
+from bqskit.ir.gates.constant.csum import CSUMGate
 from bqskit.ir.gates.constant.cx import CNOTGate
 from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.gates.parameterized.u8 import U8Gate
+from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 from bqskit.ir.location import CircuitLocation
 from bqskit.qis.graph import CouplingGraph
 from bqskit.qis.graph import CouplingGraphLike
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_valid_radixes
 
+if TYPE_CHECKING:
+    from bqskit.ir.circuit import Circuit
 
-default_gate_set: set[Gate] = {
+
+default_qubit_gate_set: set[Gate] = {
     CNOTGate(),
     U3Gate(),
 }
+default_qutrit_gate_set: set[Gate] = {
+    CSUMGate(),
+    U8Gate(),
+}
 
 
 class MachineModel:
@@ -27,7 +37,7 @@ def __init__(
         self,
         num_qudits: int,
         coupling_graph: CouplingGraphLike | None = None,
-        gate_set: set[Gate] = default_gate_set,
+        gate_set: set[Gate] | None = None,
         radixes: Sequence[int] = [],
     ) -> None:
         """
@@ -43,7 +53,8 @@ def __init__(
                 (Default: None)
 
             gate_set (set[Gate]): The native gate set available on the
-                machine.
+                machine. Defaults to CNOTs+U3s for qubits, CSUM+U8s for
+                qutrits, and variable unitary gates for arbitrary qudits.
 
             radixes (Sequence[int]): A sequence with its length equal
                 to `num_qudits`. Each element specifies the base of a
@@ -84,6 +95,15 @@ def __init__(
         ):
             raise TypeError('Invalid coupling graph, expected list of tuples')
 
+        if gate_set is None:
+            r = self.radixes[0]
+            if r == 2:
+                gate_set = default_qubit_gate_set
+            elif r == 3:
+                gate_set = default_qutrit_gate_set
+            else:
+                gate_set = {VariableUnitaryGate(2, [r, r])}
+
         if not isinstance(gate_set, set):
             raise TypeError(f'Expected set of gates, got {type(gate_set)}.')
 
@@ -98,7 +118,12 @@ def get_locations(self, block_size: int) -> list[CircuitLocation]:
         """Return all `block_size` connected blocks of qudit indicies."""
         return self.coupling_graph.get_subgraphs_of_size(block_size)
 
-    def is_compatible(self, circuit: Circuit) -> bool:
+    # TODO: add placement support
+    def is_compatible(
+        self,
+        circuit: Circuit,
+        placement: list[int] | None = None,
+    ) -> bool:
         """Check if a circuit is compatible with this model."""
         if circuit.num_qudits > self.num_qudits:
             return False
diff --git a/bqskit/compiler/passdata.py b/bqskit/compiler/passdata.py
new file mode 100644
index 000000000..85b027e66
--- /dev/null
+++ b/bqskit/compiler/passdata.py
@@ -0,0 +1,201 @@
+"""This module implements the PassData class."""
+from __future__ import annotations
+
+import copy
+import itertools as it
+from typing import Any
+from typing import Iterator
+from typing import MutableMapping
+from typing import Sequence
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.ir.circuit import Circuit
+from bqskit.qis import StateVector
+from bqskit.qis import UnitaryMatrix
+from bqskit.qis.graph import CouplingGraph
+from bqskit.qis.state.system import StateSystem
+from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_real_number
+from bqskit.utils.typing import is_sequence
+
+
+class PassData(MutableMapping[str, Any]):
+    """
+    A dictionary wrapper shared between all passes in a compilation run.
+
+    This class reserves certain keywords and supplies default initial values for
+    them. Other than that, it behaves very similar to a normal dictionary object
+    mapping `str` keys to any type of value.
+    """
+
+    _reserved_keys = [
+        'target',
+        'model',
+        'placement',
+        'error',
+        'seed',
+        'machine_model',
+    ]
+
+    def __init__(self, circuit: Circuit) -> None:
+        """Initialize a PassData object from `circuit`."""
+        self._target: Circuit | StateVector | UnitaryMatrix | StateSystem
+        if circuit.num_qudits <= 8:
+            self._target = circuit.get_unitary()
+        else:
+            self._target = circuit
+
+        self._error = 0.0
+        self._model = MachineModel(circuit.num_qudits)
+        self._placement = list(range(circuit.num_qudits))
+        self._data: dict[str, Any] = {}
+        self._seed: int | None = None
+
+    @property
+    def target(self) -> StateVector | UnitaryMatrix | StateSystem:
+        """Return the current target unitary or state."""
+        if isinstance(self._target, Circuit):
+            self._target = self._target.get_unitary()
+
+        return self._target
+
+    @target.setter
+    def target(self, _val: StateVector | UnitaryMatrix | StateSystem) -> None:
+        if not isinstance(_val, (StateVector, UnitaryMatrix, StateSystem)):
+            raise TypeError(
+                f'Cannot assign type {type(_val)} to target.'
+                ' Expected either a StateVector, StateSystem,'
+                ' or UnitaryMatrix.',
+            )
+        if len(self.placement) != _val.num_qudits:
+            self.placement = list(range(_val.num_qudits))
+        self._target = _val
+
+    @property
+    def error(self) -> float:
+        """Return the current target unitary or state."""
+        return self._error
+
+    @error.setter
+    def error(self, _val: float) -> None:
+        if not is_real_number(_val):
+            raise TypeError(
+                f'Cannot assign type {type(_val)} to error.'
+                ' Expected a real number.',
+            )
+
+        self._error = _val
+
+    @property
+    def model(self) -> MachineModel:
+        """Return the current target MachineModel."""
+        return self._model
+
+    @model.setter
+    def model(self, _val: MachineModel) -> None:
+        if not isinstance(_val, MachineModel):
+            raise TypeError(
+                f'Cannot set model to {type(_val)}.'
+                ' Expected a MachineModel.',
+            )
+
+        self._model = _val
+
+    @property
+    def placement(self) -> list[int]:
+        """Return the current placement of circuit qudits on model qudits."""
+        return self._placement
+
+    @placement.setter
+    def placement(self, _val: Sequence[int]) -> None:
+        if not is_sequence(_val):
+            raise TypeError(
+                f'Cannot set placement to {type(_val)}.'
+                ' Expected a sequence of integers.',
+            )
+
+        if not all(is_integer(x) for x in _val):
+            raise TypeError(
+                'Cannot set placement. Expected a sequence of integers.',
+            )
+
+        self._placement = list(int(x) for x in _val)
+
+    @property
+    def seed(self) -> int | None:
+        """Return the pass's seed."""
+        return self._seed
+
+    @seed.setter
+    def seed(self, _val: int | None) -> None:
+        if _val is not None and not is_integer(_val):
+            raise TypeError(
+                f'Cannot set seed to {type(_val)}.'
+                ' Expected an integer or none.',
+            )
+        self._seed = _val
+
+    @property
+    def connectivity(self) -> CouplingGraph:
+        """Retrieve the physical connectivity of the circuit qudits."""
+        return self.model.coupling_graph.get_subgraph(self.placement)
+
+    def __getitem__(self, _key: str) -> Any:
+        """Retrieve the value associated with `_key` from the pass data."""
+        if _key in self._reserved_keys:
+            if _key == 'machine_model':
+                _key = 'model'
+            return self.__getattribute__(_key)
+
+        return self._data.__getitem__(_key)
+
+    def __setitem__(self, _key: str, _val: Any) -> None:
+        """Update the value associated with `_key` in the pass data."""
+        if _key in self._reserved_keys:
+            if _key == 'machine_model':
+                _key = 'model'
+            return self.__setattr__(_key, _val)
+
+        return self._data.__setitem__(_key, _val)
+
+    def __delitem__(self, _key: str) -> None:
+        """Delete the key-value pair associated with `_key`."""
+        if _key in self._reserved_keys:
+            raise RuntimeError(f'Cannot delete {_key} from data.')
+
+        return self._data.__delitem__(_key)
+
+    def __iter__(self) -> Iterator[str]:
+        """Return an iterator over all keys in the pass data."""
+        return it.chain(self._reserved_keys.__iter__(), self._data.__iter__())
+
+    def __len__(self) -> int:
+        """Return the number of key-value pairs in the pass data."""
+        return self._data.__len__() + len(self._reserved_keys)
+
+    def __contains__(self, _o: object) -> bool:
+        """Return true if `_o` is a key in the pass data."""
+        in_resv = self._reserved_keys.__contains__(_o)
+        in_data = self._data.__contains__(_o)
+        return in_resv or in_data
+
+    def copy(self) -> PassData:
+        """Returns a deep copy of the data."""
+        return copy.deepcopy(self)
+
+    def become(self, other: PassData, deepcopy: bool = False) -> None:
+        """Become a copy of `other`."""
+        if deepcopy:
+            self._target = copy.deepcopy(other._target)
+            self._error = copy.deepcopy(other._error)
+            self._model = copy.deepcopy(other._model)
+            self._placement = copy.deepcopy(other._placement)
+            self._data = copy.deepcopy(other._data)
+            self._seed = copy.deepcopy(other._seed)
+        else:
+            self._target = copy.copy(other._target)
+            self._error = copy.copy(other._error)
+            self._model = copy.copy(other._model)
+            self._placement = copy.copy(other._placement)
+            self._data = copy.copy(other._data)
+            self._seed = copy.copy(other._seed)
diff --git a/bqskit/compiler/status.py b/bqskit/compiler/status.py
new file mode 100644
index 000000000..74c94a355
--- /dev/null
+++ b/bqskit/compiler/status.py
@@ -0,0 +1,11 @@
+"""This module implements the CompilationStatus enum."""
+from __future__ import annotations
+
+from enum import IntEnum
+
+
+class CompilationStatus(IntEnum):
+    """The status of a CompilationTask."""
+    UNKNOWN = 0
+    RUNNING = 1
+    DONE = 2
diff --git a/bqskit/compiler/task.py b/bqskit/compiler/task.py
index 06da28720..08d398ec9 100644
--- a/bqskit/compiler/task.py
+++ b/bqskit/compiler/task.py
@@ -3,24 +3,14 @@
 
 import logging
 import uuid
-import warnings
-from typing import Sequence
-
-from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates.circuitgate import CircuitGate
-from bqskit.ir.operation import Operation
-from bqskit.passes.control import ForEachBlockPass
-from bqskit.passes.control.predicates.count import GateCountPredicate
-from bqskit.passes.control.whileloop import WhileLoopPass
-from bqskit.passes.partitioning.cluster import ClusteringPartitioner
-from bqskit.passes.partitioning.quick import QuickPartitioner
-from bqskit.passes.processing import ScanningGateRemovalPass
-from bqskit.passes.synthesis import QFASTDecompositionPass
-from bqskit.passes.synthesis.leap import LEAPSynthesisPass
-from bqskit.passes.util import UnfoldPass
-from bqskit.qis.unitary.unitarymatrix import UnitaryLike
+from typing import TYPE_CHECKING
+
+from bqskit.compiler.passdata import PassData
+from bqskit.compiler.workflow import Workflow
+from bqskit.compiler.workflow import WorkflowLike
+
+if TYPE_CHECKING:
+    from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
 
@@ -33,92 +23,40 @@ class CompilationTask():
     can be submitted to a BQSKit compiler to be efficiently executed.
     """
 
-    def __init__(self, input: Circuit, passes: Sequence[BasePass]) -> None:
+    def __init__(self, input: Circuit, workflow: WorkflowLike) -> None:
         """
         Construct a CompilationTask.
 
         Args:
             input (Circuit): The input circuit to be compiled.
 
-            passes (Sequence[BasePass]): The configured operations to be
+            workflow (WorkflowLike): The configured workflow to be
                 performed on the circuit.
         """
         self.task_id = uuid.uuid4()
-        self.input_circuit = input
-        self.passes = passes
-
-    @staticmethod
-    def synthesize(utry: UnitaryLike) -> CompilationTask:
-        """Produces a standard synthesis task for the given unitary."""
-        warnings.warn(
-            'Default task creation is deprecated and will soon be removed.\n'
-            'Instead, use the new compile function.',
-        )
-        circuit = Circuit.from_unitary(utry)
-        num_qudits = circuit.num_qudits
-
-        if num_qudits > 6:
-            _logger.warning('Synthesis input size is very large.')
-
-        inner_seq = [
-            LEAPSynthesisPass(),
-            ScanningGateRemovalPass(),
-        ]
-
-        passes: list[BasePass] = []
-        if num_qudits >= 5:
-            passes.append(QFASTDecompositionPass())
-            passes.append(
-                ForEachBlockPass(
-                    inner_seq,
-                    replace_filter=less_2q_gates,
-                ),
-            )
-            passes.append(UnfoldPass())
-        else:
-            passes.extend(inner_seq)
-
-        return CompilationTask(circuit, passes)
-
-    @staticmethod
-    def optimize(circuit: Circuit) -> CompilationTask:
-        """Produces a standard optimization task for the given circuit."""
-        warnings.warn(
-            'Default task creation is deprecated and will soon be removed.\n'
-            'Instead, use the new compile function.',
-        )
-        num_qudits = circuit.num_qudits
-
-        if num_qudits <= 3:
-            return CompilationTask.synthesize(circuit.get_unitary())
-
-        inner_seq = [
-            LEAPSynthesisPass(),
-            ScanningGateRemovalPass(),
-        ]
-
-        passes: list[BasePass] = []
-        passes.append(QuickPartitioner(3))
-        passes.append(ForEachBlockPass(inner_seq, replace_filter=less_2q_gates))
-        passes.append(UnfoldPass())
-
-        iterative_reopt = WhileLoopPass(
-            GateCountPredicate(CNOTGate()),
-            [
-                ClusteringPartitioner(3, 4),
-                ForEachBlockPass(inner_seq, replace_filter=less_2q_gates),
-                UnfoldPass(),
-            ],
-        )
-
-        passes.append(iterative_reopt)
-        return CompilationTask(circuit, passes)
-
-
-def less_2q_gates(circuit: Circuit, op: Operation) -> bool:
-    """Replace `circuit' with `op` if has less 2 qubit gates."""
-    if not isinstance(op, CircuitGate):
-        return True
-    og_num_2q_gate = len([op for op in op._circuit if op.num_qudits >= 2])
-    new_num_2q_gate = len([op for op in circuit if op.num_qudits >= 2])
-    return new_num_2q_gate > og_num_2q_gate
+        self.circuit = input
+        self.workflow = Workflow(workflow)
+
+        self.data = PassData(input)
+        """The task's data for use in BQSKit passes."""
+
+        self.done = False
+        """True when the task is complete."""
+
+        self.request_data = False
+        """If true, :func:`run` will additionally return the PassData."""
+
+        self.logging_level: int | None = None
+        """A general filter on all logging messages in the system."""
+
+        self.max_logging_depth = -1
+        """No logging for tasks with more than `max_logging_depth` parents."""
+
+    async def run(self) -> Circuit | tuple[Circuit, PassData]:
+        """Execute the task."""
+        await self.workflow.run(self.circuit, self.data)
+
+        if not self.request_data:
+            return self.circuit
+
+        return self.circuit, self.data
diff --git a/bqskit/compiler/workflow.py b/bqskit/compiler/workflow.py
new file mode 100644
index 000000000..603ae2474
--- /dev/null
+++ b/bqskit/compiler/workflow.py
@@ -0,0 +1,115 @@
+"""This module implements the Workflow class."""
+from __future__ import annotations
+
+import copy
+from typing import Iterable
+from typing import Iterator
+from typing import overload
+from typing import Sequence
+from typing import Union
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.utils.random import seed_random_sources
+from bqskit.utils.typing import is_iterable
+
+
+class Workflow(BasePass, Sequence[BasePass]):
+    """A BQSKit workflow captures a quantum circuit compilation process."""
+
+    def __init__(self, passes: WorkflowLike) -> None:
+        """
+        Initialize a workflow object from a sequence of passes.
+
+        Raises:
+            ValueError: If passes is empty.
+        """
+        if isinstance(passes, Workflow):
+            self._passes: list[BasePass] = copy.deepcopy(passes._passes)
+            return
+
+        if isinstance(passes, BasePass):
+            passes = [passes]
+
+        if not is_iterable(passes):
+            msg = f'Expected Pass or sequence of Passes, got {type(passes)}.'
+            raise TypeError(msg)
+
+        if not all(isinstance(p, BasePass) for p in passes):
+            truth_list = [isinstance(p, BasePass) for p in passes]
+            wrong_type = type(list(passes)[truth_list.index(False)])
+            msg = f'Expected Pass or sequence of Passes, got {wrong_type}.'
+            raise TypeError(msg)
+
+        self._passes = list(passes)
+
+        if len(self._passes) == 0:
+            raise ValueError('Expected at least one pass in workflow.')
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        for pass_obj in self._passes:
+            if data.seed is not None:
+                seed_random_sources(data.seed)
+            await pass_obj.run(circuit, data)
+
+    # @staticmethod
+    # def build_compile_flow() -> Workflow:
+    #     pass
+
+    # @staticmethod
+    # def build_transpile_flow() -> Workflow:
+    #     pass
+
+    # @staticmethod
+    # def build_synthesize_flow() -> Workflow:
+    #     pass
+
+    # @staticmethod
+    # def build_map_flow() -> Workflow:
+    #     pass
+
+    # @staticmethod
+    # def build_prepare_flow() -> Workflow:
+    #     pass
+
+    def save(self, filename: str) -> None:
+        import pickle
+        with open(filename, 'wb') as f:
+            pickle.dump(self, f)
+
+    @staticmethod
+    def load(filename: str) -> Workflow:
+        import pickle
+        with open(filename, 'rb') as f:
+            return pickle.load(f)
+
+    # def __str__(self) -> str:
+    #     pass  # TODO:
+
+    # def __repr__(self) -> str:
+    #     pass  # TODO:
+
+    def __add__(self, other: Workflow) -> Workflow:
+        return Workflow(self._passes + other._passes)
+
+    def __len__(self) -> int:
+        return self._passes.__len__()
+
+    def __iter__(self) -> Iterator[BasePass]:
+        return self._passes.__iter__()
+
+    @overload
+    def __getitem__(self, _key: int, /) -> BasePass:
+        ...
+
+    @overload
+    def __getitem__(self, _key: slice, /) -> list[BasePass]:
+        ...
+
+    def __getitem__(self, _key: int | slice) -> BasePass | list[BasePass]:
+        return self._passes.__getitem__(_key)
+
+
+WorkflowLike = Union[Workflow, Iterable[BasePass], BasePass]
diff --git a/bqskit/exec/runners/quest.py b/bqskit/exec/runners/quest.py
index af1f8e6e4..fac31c3ce 100644
--- a/bqskit/exec/runners/quest.py
+++ b/bqskit/exec/runners/quest.py
@@ -9,7 +9,6 @@
 import numpy.typing as npt
 from scipy.optimize import dual_annealing
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.compiler import MachineModel
 from bqskit.exec.results import RunnerResults
@@ -104,17 +103,17 @@ def run(self, circuit: Circuit) -> RunnerResults:
         if self.model is not None:
             pass_list.insert(0, SetModelPass(self.model))
 
-        task = CompilationTask(circuit.copy(), pass_list)
-
         started = False
         if self.compiler is None:
             self.compiler = Compiler()
             started = True
-        # self.compiler = cast(Compiler, self.compiler)
-        blocked_circuit = self.compiler.compile(task)
+
+        blocked_circuit, data = self.compiler.compile(
+            circuit.copy(), pass_list, True,
+        )
 
         # 2. Gather partial solutions
-        data = self.compiler.analyze(task, ForEachBlockPass.key)
+        data = data[ForEachBlockPass.key]
         psols, pts = self.parse_data(blocked_circuit, data)
         # psols: psols[i] = list[[circuit, dist]] -> block i's partial solutions
         # pts: pts[i] = CircuitPoint -> block i's locations
@@ -358,14 +357,13 @@ def gen_approximate_circuits(
     if model is not None:
         pass_list.insert(0, SetModelPass(model))
 
-    task = CompilationTask(circuit.copy(), pass_list)
-
     started = False
     if compiler is None:
         compiler = Compiler()
         started = True
-    # compiler = cast(Compiler, compiler)
-    blocked_circuit = compiler.compile(task)
+
+    blocked_circuit, data = compiler.compile(circuit.copy(), pass_list, True)
+    data = data[ForEachBlockPass.key]
 
     # 2. Gather partial solutions
     runner = QuestRunner(
@@ -380,7 +378,6 @@ def gen_approximate_circuits(
         instantiate_options,
     )
 
-    data = compiler.analyze(task, ForEachBlockPass.key)
     psols, pts = runner.parse_data(blocked_circuit, data)
     # psols: psols[i] = list[[circuit, dist]] -> block i's partial solutions
     # pts: pts[i] = CircuitPoint -> block i's locations
diff --git a/bqskit/ir/circuit.py b/bqskit/ir/circuit.py
index d0932f884..3a5903160 100644
--- a/bqskit/ir/circuit.py
+++ b/bqskit/ir/circuit.py
@@ -19,8 +19,6 @@
 
 import numpy as np
 import numpy.typing as npt
-from distributed import get_client
-from distributed import secede
 
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates.circuitgate import CircuitGate
@@ -47,6 +45,7 @@
 from bqskit.qis.state.state import StateLike
 from bqskit.qis.state.state import StateVector
 from bqskit.qis.state.statemap import StateVectorMap
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.differentiable import DifferentiableUnitary
 from bqskit.qis.unitary.unitary import RealVector
 from bqskit.qis.unitary.unitarybuilder import UnitaryBuilder
@@ -57,12 +56,11 @@
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_iterable
 from bqskit.utils.typing import is_sequence_of_int
-from bqskit.utils.typing import is_square_matrix
 from bqskit.utils.typing import is_valid_radixes
-from bqskit.utils.typing import is_vector
 
 if TYPE_CHECKING:
     from bqskit.ir.opt.cost.function import CostFunction
+    from bqskit.compiler.basepass import BasePass
 
 _logger = logging.getLogger(__name__)
 
@@ -2591,15 +2589,53 @@ def get_unitary_and_grad(
 
         return left.get_unitary(), np.array(full_grads)
 
+    def perform(
+        self,
+        compiler_pass: BasePass,
+        data: dict[str, Any] | None = None,
+    ) -> None:
+        """
+        Execute the provided `compiler_pass` on this circuit.
+
+        This function is necessary since BQSKit Pass objects cannot have
+        their :func:`~bqskit.compiler.basepass.run` function directly called
+        on a circuit.
+
+        Args:
+            compiler_pass (BasePass): The BQSKit pass to perform on this
+                circuit.
+
+            data (dict[str, Any] | None): Optionally provide additional
+                pass data to the compiler pass.
+
+        Note:
+            You cannot perform a pass using this method while a local
+            :class:`~bqskit.compiler.compiler.Compiler` object is live.
+            Rather, you should use the
+            :class:`~bqskit.compiler.compiler.Compiler` directly.
+        """
+        from bqskit.compiler.compiler import Compiler
+        from bqskit.compiler.passdata import PassData
+        from bqskit.compiler.task import CompilationTask
+
+        pass_data = PassData(self)
+        if data is not None:
+            pass_data.update(data)
+
+        with Compiler() as compiler:
+            task = CompilationTask(self, [compiler_pass])
+            task.data = pass_data
+            task_id = compiler.submit(task)
+            self.become(compiler.result(task_id))  # type: ignore
+
     def instantiate(
         self,
-        target: StateLike | UnitaryLike,
+        target: StateLike | UnitaryLike | StateSystem,
         method: str | Instantiater | None = None,
         multistarts: int = 1,
         seed: int | None = None,
         multistart_gen: MultiStartGenerator = RandomStartGenerator(),
         score_fn_gen: CostFunctionGenerator = HilbertSchmidtCostGenerator(),
-        parallel: bool = False,
         **kwargs: Any,
     ) -> Circuit:
         """
@@ -2610,12 +2646,13 @@ def instantiate(
         state to the target state.
 
         Args:
-            target (StateLike | UnitaryLike): The target unitary or state.
-                If a unitary is specified, the method changes the circuit's
-                parameters in an effort to get closer to implementing the
-                target. If a state is specified, the method changes the
-                circuit's parameters in an effort to get closer to producing
-                the target state when starting from the zero state.
+            target (StateLike | UnitaryLike | StateSystem): The target unitary
+                or state. If a unitary is specified, the method changes
+                the circuit's parameters in an effort to get closer to
+                implementing the target. If a state is specified, the
+                method changes the circuit's parameters in an effort to
+                get closer to producing the target state when starting
+                from the zero state.
 
             method (str | Instantiater | None): The method with which to
                 instantiate the circuit. Currently, `"qfactor"` and
@@ -2624,8 +2661,7 @@ def instantiate(
                 directly through this.
 
             multistarts (int): The number of starting points to sample
-                instantiation with. If `parallel` is True and this is greater
-                than one, will spawn this many Dask tasks. (Default: 1)
+                instantiation with. (Default: 1)
 
             seed (int | None): The seed for any pseudo-random number generators
                 to use. Note that this is not guaranteed to make this method
@@ -2640,9 +2676,7 @@ def instantiate(
                 from the different starting points.
                 (Default: HilbertSchmidtCostGenerator())
 
-            parallel (bool): If True and `multistarts` is greater than 1,
-                this will attempt to connect to a dask cluster and submit
-                jobs to be run in parallel. (Default: False)
+            parallel (bool): Ignored for now.
 
             kwargs (dict[str, Any]): Method specific options, passed
                 directly to method constructor. For more info, see
@@ -2719,15 +2753,27 @@ def instantiate(
         instantiater = cast(Instantiater, instantiater)
 
         # Check Target
-        if is_square_matrix(target):
-            target = UnitaryMatrix(target)  # type: ignore
-        elif is_vector(target):
-            target = StateVector(target)  # type: ignore
-        else:
+        try:
+            if UnitaryMatrix.is_unitary(target):
+                target = UnitaryMatrix(target)
+
+            elif StateVector.is_pure_state(target):
+                target = StateVector(target)
+
+            elif StateSystem.is_state_system(target):
+                target = StateSystem(target)
+
+            else:
+                raise TypeError(
+                    'Target is neither a unitary, a state system'
+                    f', nor a state. Got {type(input)}.',
+                )
+        except Exception as e:
             raise TypeError(
-                'Expected either StateVector or UnitaryMatrix'
-                ' for target, got %s.' % type(target),
-            )
+                'Unable to determine type of input.'
+                ' Ensure that you are trying to compile a valid'
+                ' circuit, unitary, or state.',
+            ) from e
 
         if target.dim != self.dim:
             raise ValueError('Target dimension mismatch with circuit.')
@@ -2750,60 +2796,60 @@ def instantiate(
         cost_fn = score_fn_gen.gen_cost(self, target)
 
         # Instantiate the circuit
-        if parallel and multistarts > 1:
-            client = get_client()
-
-            def single_start_instantiate(
-                instantiater: Instantiater,
-                circuit: Circuit,
-                target: UnitaryMatrix,
-                start: npt.NDArray[np.float64],
-            ) -> npt.NDArray[np.float64]:
-                return instantiater.instantiate(circuit, target, start)
-
-            def scoring_fn(
-                fn_gen: CostFunctionGenerator,
-                circuit: Circuit,
-                target: UnitaryMatrix,
-                params: npt.NDArray[np.float64],
-            ) -> float:
-                return fn_gen.gen_cost(circuit, target).get_cost(params)
-
-            param_futures = client.map(
-                single_start_instantiate,
-                [instantiater] * multistarts,
-                [self] * multistarts,
-                [target] * multistarts,
-                starts,
-                pure=False,
-            )
-
-            score_futures = client.map(
-                scoring_fn,
-                [score_fn_gen] * multistarts,
-                [self] * multistarts,
-                [target] * multistarts,
-                param_futures,
-                pure=False,
-            )
-
-            # We only want to secede on worker threads, so try to recover if
-            # Circuit.instantiate is called from the main thread
-            try:
-                secede()
-            except ValueError:
-                pass
-
-            scores = client.gather(score_futures)
-            best_index = scores.index(min(scores))
-            params = param_futures[best_index].result()
-
-        else:
-            params_list = [
-                instantiater.instantiate(self, target, start)
-                for start in starts
-            ]
-            params = sorted(params_list, key=lambda x: cost_fn(x))[0]
+        # if parallel and multistarts > 1:
+        #     client = get_client()
+
+        #     def single_start_instantiate(
+        #         instantiater: Instantiater,
+        #         circuit: Circuit,
+        #         target: UnitaryMatrix,
+        #         start: npt.NDArray[np.float64],
+        #     ) -> npt.NDArray[np.float64]:
+        #         return instantiater.instantiate(circuit, target, start)
+
+        #     def scoring_fn(
+        #         fn_gen: CostFunctionGenerator,
+        #         circuit: Circuit,
+        #         target: UnitaryMatrix,
+        #         params: npt.NDArray[np.float64],
+        #     ) -> float:
+        #         return fn_gen.gen_cost(circuit, target).get_cost(params)
+
+        #     param_futures = client.map(
+        #         single_start_instantiate,
+        #         [instantiater] * multistarts,
+        #         [self] * multistarts,
+        #         [target] * multistarts,
+        #         starts,
+        #         pure=False,
+        #     )
+
+        #     score_futures = client.map(
+        #         scoring_fn,
+        #         [score_fn_gen] * multistarts,
+        #         [self] * multistarts,
+        #         [target] * multistarts,
+        #         param_futures,
+        #         pure=False,
+        #     )
+
+        #     # We only want to secede on worker threads, so try to recover if
+        #     # Circuit.instantiate is called from the main thread
+        #     try:
+        #         secede()
+        #     except ValueError:
+        #         pass
+
+        #     scores = client.gather(score_futures)
+        #     best_index = scores.index(min(scores))
+        #     params = param_futures[best_index].result()
+
+        # else:
+        params_list = [
+            instantiater.instantiate(self, target, start)
+            for start in starts
+        ]
+        params = sorted(params_list, key=lambda x: cost_fn(x))[0]
 
         # Return best result
         self.set_params(params)
diff --git a/bqskit/ir/opt/cost/functions/cost/hilbertschmidt.py b/bqskit/ir/opt/cost/functions/cost/hilbertschmidt.py
index 783a72677..d84c8617c 100644
--- a/bqskit/ir/opt/cost/functions/cost/hilbertschmidt.py
+++ b/bqskit/ir/opt/cost/functions/cost/hilbertschmidt.py
@@ -9,6 +9,7 @@
 from bqskit.ir.opt.cost.differentiable import DifferentiableCostFunction
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 
 if TYPE_CHECKING:
     from bqskit.ir.circuit import Circuit
@@ -40,7 +41,7 @@ class HilbertSchmidtCostGenerator(CostFunctionGenerator):
     def gen_cost(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> CostFunction:
         """Generate a CostFunction, see CostFunctionGenerator for more info."""
         return HilbertSchmidtCost(circuit, target)
diff --git a/bqskit/ir/opt/cost/functions/residuals/hilbertschmidt.py b/bqskit/ir/opt/cost/functions/residuals/hilbertschmidt.py
index d6799438b..ff5e6e09c 100644
--- a/bqskit/ir/opt/cost/functions/residuals/hilbertschmidt.py
+++ b/bqskit/ir/opt/cost/functions/residuals/hilbertschmidt.py
@@ -8,6 +8,7 @@
 from bqskit.ir.opt.cost.differentiable import DifferentiableResidualsFunction
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 
 if TYPE_CHECKING:
     from bqskit.ir.circuit import Circuit
@@ -39,7 +40,7 @@ class HilbertSchmidtResidualsGenerator(CostFunctionGenerator):
     def gen_cost(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> CostFunction:
         """Generate a CostFunction, see CostFunctionGenerator for more info."""
         return HilbertSchmidtResiduals(circuit, target)
diff --git a/bqskit/ir/opt/cost/generator.py b/bqskit/ir/opt/cost/generator.py
index 969badd12..85432f824 100644
--- a/bqskit/ir/opt/cost/generator.py
+++ b/bqskit/ir/opt/cost/generator.py
@@ -4,6 +4,8 @@
 import abc
 from typing import TYPE_CHECKING
 
+from bqskit.qis.state.system import StateSystem
+
 if TYPE_CHECKING:
     from bqskit.ir.opt.cost import CostFunction
     from bqskit.ir.circuit import Circuit
@@ -30,7 +32,7 @@ class CostFunctionGenerator(abc.ABC):
     def gen_cost(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> CostFunction:
         """
         Generate a function from a circuit and target that maps params to cost.
@@ -38,7 +40,8 @@ def gen_cost(
         Args:
             circuit (Circuit): The circuit the cost function is generated for.
 
-            target (UnitaryMatrix | StateVector): The target object.
+            target (UnitaryMatrix | StateVector | StateSystem): The target
+                object.
 
         Returns:
             (CostFunction): The primitive cost function
@@ -49,7 +52,7 @@ def gen_cost(
     def calc_cost(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Generate and calculate the cost from the CostFunction."""
         return self.gen_cost(circuit, target).get_cost(circuit.params)
@@ -57,7 +60,7 @@ def calc_cost(
     def __call__(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Generate and calculate the cost from the CostFunction."""
         return self.calc_cost(circuit, target)
diff --git a/bqskit/ir/opt/instantiater.py b/bqskit/ir/opt/instantiater.py
index 1ee6a3a39..8cf15b084 100644
--- a/bqskit/ir/opt/instantiater.py
+++ b/bqskit/ir/opt/instantiater.py
@@ -8,11 +8,13 @@
 import numpy.typing as npt
 
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 if TYPE_CHECKING:
     from bqskit.ir.circuit import Circuit
     from bqskit.qis.state.state import StateLike
+    from bqskit.qis.state.system import StateSystemLike
     from bqskit.qis.unitary.unitarymatrix import UnitaryLike
 
 
@@ -28,7 +30,7 @@ class Instantiater(abc.ABC):
     def instantiate(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
         x0: npt.NDArray[np.float64],
     ) -> npt.NDArray[np.float64]:
         """
@@ -37,8 +39,8 @@ def instantiate(
         Args:
             circuit (Circuit): The circuit template to instantiate.
 
-            target (UnitaryMatrix | StateVector): The unitary matrix to
-                implement or state to prepare.
+            target (UnitaryMatrix | StateVector | StateSystem): The unitary
+                matrix to implement or state to prepare.
 
             x0 (np.ndarray): Initial point to use during instantiation.
 
@@ -73,21 +75,33 @@ def get_violation_report(circuit: Circuit) -> str:
 
     def check_target(
         self,
-        target: UnitaryLike | StateLike,
-    ) -> UnitaryMatrix | StateVector:
+        target: UnitaryLike | StateLike | StateSystemLike,
+    ) -> UnitaryMatrix | StateVector | StateSystem:
         """Check `target` to be valid and return it casted."""
+        # Check `target`
         try:
-            typed_target = StateVector(target)  # type: ignore
-        except (ValueError, TypeError):
-            try:
-                typed_target = UnitaryMatrix(target)  # type: ignore
-            except (ValueError, TypeError) as ex:
-                raise TypeError(
-                    'Expected either StateVector, UnitaryMatrix, or'
-                    ' CostFunction for target, got %s.' % type(target),
-                ) from ex
+            if UnitaryMatrix.is_unitary(target):
+                target = UnitaryMatrix(target)
+
+            elif StateVector.is_pure_state(target):
+                target = StateVector(target)
 
-        return typed_target
+            elif StateSystem.is_state_system(target):
+                target = StateSystem(target)
+
+            else:
+                raise TypeError(
+                    'Target is neither a unitary, a state system'
+                    f', nor a state. Got {type(input)}.',
+                )
+        except Exception as e:
+            raise TypeError(
+                'Unable to determine type of input.'
+                ' Ensure that you are trying to compile a valid'
+                ' circuit, unitary, or state.',
+            ) from e
+
+        return target
 
     @staticmethod
     @abc.abstractmethod
diff --git a/bqskit/ir/opt/instantiaters/minimization.py b/bqskit/ir/opt/instantiaters/minimization.py
index bab584f34..e245cbc3f 100644
--- a/bqskit/ir/opt/instantiaters/minimization.py
+++ b/bqskit/ir/opt/instantiaters/minimization.py
@@ -13,6 +13,7 @@
 from bqskit.ir.opt.minimizer import Minimizer
 from bqskit.ir.opt.minimizers.ceres import CeresMinimizer
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 if TYPE_CHECKING:
@@ -25,7 +26,7 @@ class Minimization(Instantiater):
     def __init__(
         self,
         cost_fn_gen: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
-        minimizer: Minimizer | None = None,
+        minimizer: Minimizer = CeresMinimizer(),
         **kwargs: dict[str, Any],  # TODO: handle dist_tol and other options
     ) -> None:
         """
@@ -36,7 +37,7 @@ def __init__(
                 functions that are minimized.
                 (Default: HilbertSchmidtGenerator())
 
-            minimizer (Minimizer | None): The minimizer to use. If left as
+            minimizer (Minimizer): The minimizer to use. If left as
                 None, attempts to select best one.
         """
 
@@ -45,20 +46,16 @@ def __init__(
                 'Expected CostFunctionGenerator, got %s.' % type(cost_fn_gen),
             )
 
-        if minimizer is not None and not isinstance(minimizer, Minimizer):
+        if not isinstance(minimizer, Minimizer):
             raise TypeError('Expected Minimizer, got %s.' % type(minimizer))
 
-        # Default to the fast CeresMinimizer
-        if minimizer is None:
-            minimizer = CeresMinimizer()
-
         self.cost_fn_gen = cost_fn_gen
         self.minimizer = minimizer
 
     def instantiate(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
         x0: npt.NDArray[np.float64],
     ) -> npt.NDArray[np.float64]:
         """Instantiate `circuit`, see Instantiater for more info."""
diff --git a/bqskit/ir/opt/instantiaters/qfactor.py b/bqskit/ir/opt/instantiaters/qfactor.py
index f769ea6e0..0b9a86a84 100644
--- a/bqskit/ir/opt/instantiaters/qfactor.py
+++ b/bqskit/ir/opt/instantiaters/qfactor.py
@@ -12,6 +12,7 @@
 
 from bqskit.ir.opt.instantiater import Instantiater
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary import LocallyOptimizableUnitary
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
@@ -38,7 +39,7 @@ def __new__(cls, **kwargs: dict[str, Any]) -> Any:
     def instantiate(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
         x0: npt.NDArray[np.float64],
     ) -> npt.NDArray[np.float64]:
         """Instantiate `circuit`, see Instantiater for more info."""
diff --git a/bqskit/ir/opt/multistartgen.py b/bqskit/ir/opt/multistartgen.py
index 9847318dd..289b54bf3 100644
--- a/bqskit/ir/opt/multistartgen.py
+++ b/bqskit/ir/opt/multistartgen.py
@@ -4,11 +4,13 @@
 import abc
 from typing import TYPE_CHECKING
 
+
 if TYPE_CHECKING:
     import numpy as np
     import numpy.typing as npt
     from bqskit.ir.circuit import Circuit
     from bqskit.qis.state.state import StateVector
+    from bqskit.qis.state.system import StateSystem
     from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -18,7 +20,7 @@ def gen_starting_points(
         self,
         multistarts: int,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> list[npt.NDArray[np.float64]]:
         """
         Generate `multistarts` starting points for instantiation.
@@ -28,7 +30,7 @@ def gen_starting_points(
 
             circuit (Circuit): The circuit to generate the points for.
 
-            target (UnitaryMatrix | StateVector): The target.
+            target (UnitaryMatrix | StateVector | StateSystem): The target.
 
         Return:
             (list[npt.NDArray[np.float64]]): List of starting inputs for
diff --git a/bqskit/ir/opt/multistartgens/diagonal.py b/bqskit/ir/opt/multistartgens/diagonal.py
index 620797e6f..04544434c 100644
--- a/bqskit/ir/opt/multistartgens/diagonal.py
+++ b/bqskit/ir/opt/multistartgens/diagonal.py
@@ -6,6 +6,7 @@
 import numpy as np
 
 from bqskit.ir.opt.multistartgen import MultiStartGenerator
+from bqskit.qis.state.system import StateSystem
 from bqskit.utils.typing import is_integer
 
 if TYPE_CHECKING:
@@ -22,7 +23,7 @@ def gen_starting_points(
         self,
         multistarts: int,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> list[npt.NDArray[np.float64]]:
         """
         Generate `multistarts` starting points for instantiation.
@@ -32,7 +33,7 @@ def gen_starting_points(
 
             circuit (Circuit): The circuit to generate the points for.
 
-            target (UnitaryMatrix | StateVector): The target.
+            target (UnitaryMatrix | StateVector | StateSystem): The target.
 
         Return:
             (list[npt.NDArray[np.float64]]): List of starting inputs for
diff --git a/bqskit/ir/opt/multistartgens/random.py b/bqskit/ir/opt/multistartgens/random.py
index 54067adc4..968d7bb58 100644
--- a/bqskit/ir/opt/multistartgens/random.py
+++ b/bqskit/ir/opt/multistartgens/random.py
@@ -12,6 +12,7 @@
     import numpy.typing as npt
     from bqskit.ir.circuit import Circuit
     from bqskit.qis.state.state import StateVector
+    from bqskit.qis.state.system import StateSystem
     from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -22,7 +23,7 @@ def gen_starting_points(
         self,
         multistarts: int,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> list[npt.NDArray[np.float64]]:
         """
         Generate `multistarts` starting points for instantiation.
diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py
index 253a96f7e..a6eac3620 100644
--- a/bqskit/passes/__init__.py
+++ b/bqskit/passes/__init__.py
@@ -45,7 +45,6 @@
     Rebase2QuditGatePass
     ScanningGateRemovalPass
     SubstitutePass
-    WindowOptimizationPass
 
 .. rubric:: Control Passes
 
@@ -210,7 +209,6 @@
 from bqskit.passes.processing.rebase import Rebase2QuditGatePass
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
 from bqskit.passes.processing.substitute import SubstitutePass
-from bqskit.passes.processing.window import WindowOptimizationPass
 from bqskit.passes.rules.ch2cnot import CHToCNOTPass
 from bqskit.passes.rules.cnot2ch import CNOTToCHPass
 from bqskit.passes.rules.cnot2cy import CNOTToCYPass
@@ -237,6 +235,7 @@
 from bqskit.passes.synthesis.qpredict import QPredictDecompositionPass
 from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.passes.synthesis.target import SetTargetPass
 from bqskit.passes.util.compress import CompressPass
 from bqskit.passes.util.conversion import BlockConversionPass
 from bqskit.passes.util.converttou3 import ToU3Pass
@@ -275,7 +274,6 @@
     'UpdateDataPass',
     'ToU3Pass',
     'ScanningGateRemovalPass',
-    'WindowOptimizationPass',
     'SimpleLayerGenerator',
     'AStarHeuristic',
     'GreedyHeuristic',
@@ -329,4 +327,5 @@
     'CNOTToCHPass',
     'CNOTToCYPass',
     'CYToCNOTPass',
+    'SetTargetPass',
 ]
diff --git a/bqskit/passes/alias.py b/bqskit/passes/alias.py
index 5114a0ba4..6f59e597e 100644
--- a/bqskit/passes/alias.py
+++ b/bqskit/passes/alias.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import abc
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -15,7 +15,7 @@ class PassAlias(BasePass):
     def get_passes(self) -> list[BasePass]:
         """Return the passes that should be run, when this alias is called."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         for bqskit_pass in self.get_passes():
-            bqskit_pass.run(circuit, data)
+            await bqskit_pass.run(circuit, data)
diff --git a/bqskit/passes/control/dothendecide.py b/bqskit/passes/control/dothendecide.py
index b29a3dbbb..274034386 100644
--- a/bqskit/passes/control/dothendecide.py
+++ b/bqskit/passes/control/dothendecide.py
@@ -2,13 +2,18 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
-from typing import Callable
-from typing import Sequence
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
-from bqskit.utils.typing import is_sequence
+from bqskit.compiler.workflow import Workflow
+
+if TYPE_CHECKING:
+    from typing import Callable
+
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import WorkflowLike
+    from bqskit.ir.circuit import Circuit
+
 
 _logger = logging.getLogger(__name__)
 
@@ -17,14 +22,14 @@ class DoThenDecide(BasePass):
     """
     The DoThenDecide class.
 
-    This is a control pass that executes a pass or sequence of passes and then
-    conditionally accepts it or reverts to the original state.
+    This is a control pass that executes a workflow and then conditionally
+    accepts the resulting circuit or reverts to the original state.
     """
 
     def __init__(
         self,
         condition: Callable[[Circuit, Circuit], bool],
-        pass_list: BasePass | Sequence[BasePass],
+        workflow: WorkflowLike,
     ) -> None:
         """
         Construct a DoThenDecide.
@@ -32,47 +37,37 @@ def __init__(
         Args:
             condition (Callable[[Circuit, Circuit], bool]): The condition
                 function that determines if the new circuit
-                (second parameter) after running `pass_list` should replace
+                (second parameter) after running `workflow` should replace
                 the original circuit (first parameter). If the condition
-                returns True, then replaced the original circuit with the
-                new one.
+                returns True, then replace the original circuit with the
+                new one. Note when passing callables to BQSKit passes,
+                they need to be defined at the module level (0-indent)
+                with a name (no lambdas).
 
-            pass_list (BasePass | Sequence[BasePass]): The pass or passes
-                to execute.
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+            workflow (WorkflowLike): The pass or passes to execute.
         """
-        if not is_sequence(pass_list) and not isinstance(pass_list, BasePass):
-            raise TypeError(
-                'Expected Pass or sequence of Passes, got %s.'
-                % type(pass_list),
-            )
-
-        if is_sequence(pass_list):
-            truth_list = [isinstance(elem, BasePass) for elem in pass_list]
-            if not all(truth_list):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(pass_list[truth_list.index(False)]),
-                )
-            if len(pass_list) == 0:
-                raise ValueError('Expected at least one pass.')
+        if not callable(condition):
+            bad_type = type(condition)
+            msg = f'Expected callable function for condition, got {bad_type}'
+            raise TypeError(msg)
 
         self.condition = condition
-        self.pass_list = pass_list if is_sequence(pass_list) else [pass_list]
+        self.workflow = Workflow(workflow)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        # Perform Work
+        # Store old state
         old_circuit = circuit.copy()
-        _logger.debug('Pass list executing...')
-        for bqskit_pass in self.pass_list:
-            bqskit_pass.run(circuit, data)
+        old_data = data.copy()
+
+        # Execute the workflow
+        await self.workflow.run(circuit, data)
 
+        # Evaluate condition
         if self.condition(old_circuit, circuit):
-            _logger.debug('Accepted circuit.')
+            _logger.info('Accepted circuit.')
 
         else:
             circuit.become(old_circuit)
-            _logger.debug('Rejected circuit.')
+            data.become(old_data)
+            _logger.info('Rejected circuit.')
diff --git a/bqskit/passes/control/dowhileloop.py b/bqskit/passes/control/dowhileloop.py
index a855618f1..6c0464170 100644
--- a/bqskit/passes/control/dowhileloop.py
+++ b/bqskit/passes/control/dowhileloop.py
@@ -2,13 +2,17 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
-from typing import Sequence
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
+from bqskit.compiler.workflow import Workflow
 from bqskit.passes.control.predicate import PassPredicate
-from bqskit.utils.typing import is_sequence
+
+if TYPE_CHECKING:
+    from bqskit.ir.circuit import Circuit
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import WorkflowLike
+
 
 _logger = logging.getLogger(__name__)
 
@@ -17,14 +21,14 @@ class DoWhileLoopPass(BasePass):
     """
     The DoWhileLoopPass class.
 
-    This is a control pass that executes a pass and then conditionally executes
-    it again in a loop.
+    This is a control pass that executes a workflow and then conditionally
+    executes it again in a loop.
     """
 
     def __init__(
         self,
         condition: PassPredicate,
-        loop_body: BasePass | Sequence[BasePass],
+        loop_body: WorkflowLike,
     ) -> None:
         """
         Construct a DoWhileLoopPass.
@@ -32,43 +36,22 @@ def __init__(
         Args:
             condition (PassPredicate): The condition checked.
 
-            loop_body (BasePass | Sequence[BasePass]): The pass or passes
-                to execute while `condition` is true.
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+            loop_body (WorkflowLike): The pass or passes to execute while
+                `condition` is true.
         """
-
         if not isinstance(condition, PassPredicate):
-            raise TypeError('Expected PassPredicate, got %s.' % type(condition))
-
-        if not is_sequence(loop_body) and not isinstance(loop_body, BasePass):
-            raise TypeError(
-                'Expected Pass or sequence of Passes, got %s.'
-                % type(loop_body),
-            )
-
-        if is_sequence(loop_body):
-            truth_list = [isinstance(elem, BasePass) for elem in loop_body]
-            if not all(truth_list):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(loop_body[truth_list.index(False)]),
-                )
-            if len(loop_body) == 0:
-                raise ValueError('Expected at least one pass.')
+            raise TypeError(f'Expected PassPredicate, got {type(condition)}.')
 
         self.condition = condition
-        self.loop_body = loop_body if is_sequence(loop_body) else [loop_body]
+        self.workflow = Workflow(loop_body)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        # Perform Work
+        # Do
         _logger.debug('Loop body executing...')
-        for loop_pass in self.loop_body:
-            loop_pass.run(circuit, data)
+        await self.workflow.run(circuit, data)
 
+        # While
         while self.condition(circuit, data):
             _logger.debug('Loop body executing...')
-            for loop_pass in self.loop_body:
-                loop_pass.run(circuit, data)
+            await self.workflow.run(circuit, data)
diff --git a/bqskit/passes/control/foreach.py b/bqskit/passes/control/foreach.py
index 9df8f20e1..31f677cd7 100644
--- a/bqskit/passes/control/foreach.py
+++ b/bqskit/passes/control/foreach.py
@@ -2,12 +2,14 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import Callable
-from typing import Sequence
 
+from bqskit.compiler.basepass import _sub_do_work
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
+from bqskit.compiler.workflow import Workflow
+from bqskit.compiler.workflow import WorkflowLike
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
@@ -15,8 +17,7 @@
 from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 from bqskit.ir.operation import Operation
 from bqskit.ir.point import CircuitPoint
-from bqskit.utils.typing import is_integer
-from bqskit.utils.typing import is_sequence
+from bqskit.runtime import get_runtime
 
 _logger = logging.getLogger(__name__)
 
@@ -25,8 +26,8 @@ class ForEachBlockPass(BasePass):
     """
     A pass that executes other passes on each block in the circuit.
 
-    This is a control pass that executes another pass or passes on every block
-    in the circuit. This will be done in parallel if run from a compiler.
+    This is a control pass that executes a workflow on every block in the
+    circuit. This will be done in parallel.
     """
 
     key = 'ForEachBlockPass_data'
@@ -34,18 +35,17 @@ class ForEachBlockPass(BasePass):
 
     def __init__(
         self,
-        loop_body: BasePass | Sequence[BasePass],
+        loop_body: WorkflowLike,
         calculate_error_bound: bool = False,
         collection_filter: Callable[[Operation], bool] | None = None,
         replace_filter: Callable[[Circuit, Operation], bool] | None = None,
-        batch_size: int = 1,
+        batch_size: int | None = None,
     ) -> None:
         """
         Construct a ForEachBlockPass.
 
         Args:
-            loop_body (BasePass | Sequence[BasePass]): The pass or passes
-                to execute on every block.
+            loop_body (WorkflowLike): The workflow to execute on every block.
 
             calculate_error_bound (bool): If set to true, will calculate
                 errors on blocks after running `loop_body` on them and
@@ -68,61 +68,34 @@ def __init__(
                 operation will be replaced with the new circuit.
                 Defaults to always replace.
 
-            batch_size (int): The amount of blocks to batch in one job.
-                If zero, batch all blocks in one job. (Default: 1).
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+            batch_size (int): (Deprecated).
         """
-
-        if not is_sequence(loop_body) and not isinstance(loop_body, BasePass):
-            raise TypeError(
-                'Expected Pass or sequence of Passes, got %s.'
-                % type(loop_body),
-            )
-
-        if is_sequence(loop_body):
-            truth_list = [isinstance(elem, BasePass) for elem in loop_body]
-            if not all(truth_list):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(loop_body[truth_list.index(False)]),
-                )
-            if len(loop_body) == 0:
-                raise ValueError('Expected at least one pass.')
-
-        if not is_integer(batch_size):
-            raise TypeError(
-                'Expected integer for batch_size'
-                f', got {type(batch_size)}',
-            )
-
-        if batch_size < 0:
-            raise ValueError(
-                'Expected nonnegative integer for batch_size'
-                f', got {batch_size}',
+        if batch_size is not None:
+            import warnings
+            warnings.warn(
+                'Batch size is no longer supported, this warning will'
+                ' become an error in a future update.',
+                DeprecationWarning,
             )
 
-        self.loop_body = loop_body if is_sequence(loop_body) else [loop_body]
         self.calculate_error_bound = calculate_error_bound
         self.collection_filter = collection_filter or default_collection_filter
         self.replace_filter = replace_filter or default_replace_filter
-        self.batch_size = batch_size
+        self.workflow = Workflow(loop_body)
 
         if not callable(self.collection_filter):
             raise TypeError(
                 'Expected callable method that maps Operations to booleans for'
-                ' collection_filter, got %s.' % type(self.collection_filter),
+                f' collection_filter, got {type(self.collection_filter)}.',
             )
 
         if not callable(self.replace_filter):
             raise TypeError(
                 'Expected callable method that maps Circuit and Operations to'
-                ' booleans for replace_filter'
-                ', got %s.' % type(self.replace_filter),
+                f' bools for replace_filter, got {type(self.replace_filter)}.',
             )
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         # Make room in data for block data
         if self.key not in data:
@@ -134,15 +107,19 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
             if self.collection_filter(op):
                 blocks.append((cycle, op))
 
+        # No blocks, no work
+        if len(blocks) == 0:
+            data[self.key].append([])
+            return
+
         # Get the machine model
-        model = self.get_model(circuit, data)
-        coupling_graph = self.get_connectivity(circuit, data)
+        model = data.model
+        coupling_graph = data.connectivity
 
         # Preprocess blocks
         subcircuits: list[Circuit] = []
-        block_datas: list[dict[str, Any]] = []
+        block_datas: list[PassData] = []
         for i, (cycle, op) in enumerate(blocks):
-            block_data: dict[str, Any] = {}
 
             # Form Subcircuit
             if isinstance(op.gate, CircuitGate):
@@ -151,7 +128,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
             else:
                 subcircuit = Circuit.from_operation(op)
 
-            # Form Subtopology
+            # Form Submodel
             subradixes = [circuit.radixes[q] for q in op.location]
             subnumbering = {op.location[i]: i for i in range(len(op.location))}
             submodel = MachineModel(
@@ -161,44 +138,26 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 subradixes,
             )
 
-            # Form subdata
+            # Form Subdata
+            block_data: PassData = PassData(subcircuit)
             block_data['subnumbering'] = subnumbering
-            block_data['machine_model'] = submodel
-            block_data['parallel'] = self.in_parallel(data)
+            block_data['model'] = submodel
             block_data['point'] = CircuitPoint(cycle, op.location[0])
             block_data['calculate_error_bound'] = self.calculate_error_bound
 
             subcircuits.append(subcircuit)
             block_datas.append(block_data)
 
-        # Group them into batches
-        if self.batch_size == 0:
-            batched_subcircuits = [subcircuits]
-            batched_block_datas = [block_datas]
-        else:
-            batched_subcircuits = [
-                subcircuits[i:i + self.batch_size]
-                for i in range(0, len(blocks), self.batch_size)
-            ]
-            batched_block_datas = [
-                block_datas[i:i + self.batch_size]
-                for i in range(0, len(blocks), self.batch_size)
-            ]
-
         # Do the work
-        results = self.execute(
-            data,
+        results = await get_runtime().map(
             _sub_do_work,
-            [self.loop_body] * len(batched_subcircuits),
-            batched_subcircuits,
-            batched_block_datas,
+            [self.workflow] * len(subcircuits),
+            subcircuits,
+            block_datas,
         )
 
         # Unpack results
-        completed_subcircuits, completed_block_datas = [], []
-        for batch in results:
-            completed_subcircuits.extend(list(zip(*batch))[0])
-            completed_block_datas.extend(list(zip(*batch))[1])
+        completed_subcircuits, completed_block_datas = zip(*results)
 
         # Postprocess blocks
         points: list[CircuitPoint] = []
@@ -223,7 +182,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
                 # Calculate Error
                 if self.calculate_error_bound:
-                    error_sum += block_data['error']
+                    error_sum += block_data.error
             else:
                 block_data['replaced'] = False
 
@@ -235,11 +194,8 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
         # Record error
         if self.calculate_error_bound:
-            if 'error' in data:
-                data['error'] += error_sum
-            else:
-                data['error'] = error_sum
-            _logger.debug(f"New circuit error is {data['error']}.")
+            data.error = (1 - ((1 - data.error) * (1 - error_sum)))
+            _logger.debug(f'New circuit error is {data.error}.')
 
 
 def default_collection_filter(op: Operation) -> bool:
@@ -255,25 +211,3 @@ def default_collection_filter(op: Operation) -> bool:
 
 def default_replace_filter(circuit: Circuit, op: Operation) -> bool:
     return True
-
-
-def _sub_do_work(
-    loop_body: Sequence[BasePass],
-    subcircuits: list[Circuit],
-    subdatas: list[dict[str, Any]],
-) -> list[tuple[Circuit, dict[str, Any]]]:
-    results = []
-    for subcircuit, subdata in zip(subcircuits, subdatas):
-        if subdata['calculate_error_bound']:
-            old_utry = subcircuit.get_unitary()
-
-        for loop_pass in loop_body:
-            loop_pass.run(subcircuit, subdata)
-
-        if subdata['calculate_error_bound']:
-            new_utry = subcircuit.get_unitary()
-            error = new_utry.get_distance_from(old_utry)
-            subdata['error'] = error
-
-        results.append((subcircuit, subdata))
-    return results
diff --git a/bqskit/passes/control/ifthenelse.py b/bqskit/passes/control/ifthenelse.py
index f3e98e93c..443804d31 100644
--- a/bqskit/passes/control/ifthenelse.py
+++ b/bqskit/passes/control/ifthenelse.py
@@ -2,13 +2,16 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
-from typing import Sequence
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
+from bqskit.compiler.workflow import Workflow
 from bqskit.passes.control.predicate import PassPredicate
-from bqskit.utils.typing import is_sequence
+
+if TYPE_CHECKING:
+    from bqskit.ir.circuit import Circuit
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import WorkflowLike
 
 _logger = logging.getLogger(__name__)
 
@@ -17,14 +20,14 @@ class IfThenElsePass(BasePass):
     """
     The IfThenElsePass class.
 
-    This is a control pass that conditionally executes another pass.
+    This is a control pass that conditionally executes a workflow.
     """
 
     def __init__(
         self,
         condition: PassPredicate,
-        on_true: BasePass | Sequence[BasePass],
-        on_false: BasePass | Sequence[BasePass] | None = None,
+        on_true: WorkflowLike,
+        on_false: WorkflowLike | None = None,
     ) -> None:
         """
         Construct a IfThenElsePass.
@@ -32,68 +35,27 @@ def __init__(
         Args:
             condition (PassPredicate): The condition checked.
 
-            on_true (BasePass | Sequence[BasePass]): The pass or passes
-                to execute if `condition` is true.
+            on_true (WorkflowLike): The pass or passes to execute if
+                `condition` is true.
 
-            on_false (BasePass | Sequence[BasePass] | None): If specified,
+            on_false (WorkflowLike | None): If specified,
                 the pass or passes to execute if `condition` is false.
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+                Defaults to None, which does is equivalent to a No-Op.
         """
 
         if not isinstance(condition, PassPredicate):
             raise TypeError('Expected PassPredicate, got %s.' % type(condition))
 
-        if not is_sequence(on_true) and not isinstance(on_true, BasePass):
-            raise TypeError(
-                'Expected Pass or sequence of Passes, got %s.' % type(on_true),
-            )
-
-        if is_sequence(on_true):
-            truth_list = [isinstance(elem, BasePass) for elem in on_true]
-            if not all(truth_list):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(on_true[truth_list.index(False)]),
-                )
-            if len(on_true) == 0:
-                raise ValueError('Expected at least one pass for true branch.')
-
-        if on_false is not None:
-            if not is_sequence(on_false) and not isinstance(on_false, BasePass):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(on_false),
-                )
-
-            if is_sequence(on_false):
-                truth_list = [isinstance(elem, BasePass) for elem in on_false]
-                if not all(truth_list):
-                    raise TypeError(
-                        'Expected Pass or sequence of Passes, got %s.'
-                        % type(on_false[truth_list.index(False)]),
-                    )
-                if len(on_false) == 0:
-                    raise ValueError(
-                        'Expected at least one pass for false branch.',
-                    )
-
-        if on_false is None:
-            on_false = []
-
         self.condition = condition
-        self.on_true = on_true if is_sequence(on_true) else [on_true]
-        self.on_false = on_false if is_sequence(on_false) else [on_false]
+        self.on_true = Workflow(on_true)
+        self.on_false = Workflow(on_false) if on_false is not None else None
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         if self.condition(circuit, data):
             _logger.debug('True branch taken.')
-            for branch_pass in self.on_true:
-                branch_pass.run(circuit, data)
+            await self.on_true.run(circuit, data)
 
         elif self.on_false is not None:
             _logger.debug('False branch taken.')
-            for branch_pass in self.on_false:
-                branch_pass.run(circuit, data)
+            await self.on_false.run(circuit, data)
diff --git a/bqskit/passes/control/paralleldo.py b/bqskit/passes/control/paralleldo.py
index 36b655628..42b9bbeee 100644
--- a/bqskit/passes/control/paralleldo.py
+++ b/bqskit/passes/control/paralleldo.py
@@ -2,16 +2,21 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import Callable
-from typing import Sequence
-
-from distributed import as_completed
-from distributed import worker_client
+from typing import Iterable
+from typing import TYPE_CHECKING
 
+from bqskit.compiler.basepass import _sub_do_work
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
-from bqskit.utils.typing import is_sequence
+from bqskit.compiler.workflow import Workflow
+from bqskit.runtime import get_runtime
+from bqskit.utils.typing import is_iterable
+
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import WorkflowLike
+    from bqskit.ir.circuit import Circuit
+
 
 _logger = logging.getLogger(__name__)
 
@@ -20,12 +25,14 @@ class ParallelDo(BasePass):
     """
     The ParallelDo class.
 
-    This is a control pass that executes a group of pass sequences in parallel.
+    This is a control pass that executes a sequence of workflows in parallel.
+    The branch that is accepted can either be the first to complete or one
+    selected by a provided ordering.
     """
 
     def __init__(
         self,
-        pass_sequences: Sequence[BasePass | Sequence[BasePass]],
+        pass_sequences: Iterable[WorkflowLike],
         less_than: Callable[[Circuit, Circuit], bool],
         pick_fisrt: bool = False,
     ) -> None:
@@ -33,8 +40,8 @@ def __init__(
         Construct a ParallelDo.
 
         Args:
-            pass_sequences (Sequence[BasePass | Sequence[BasePass]]):
-                The group of pass sequences to run in parallel.
+            pass_sequences (Iterable[WorkflowLike]): The group of workflows
+                to run in parallel.
 
             less_than (Callable[[Circuit, Circuit], bool]): Return True
                 if the first circuit is preferred to the second one.
@@ -42,84 +49,53 @@ def __init__(
                 select.
 
             pick_first (bool): If true, then the pass will complete as
-                soon as one of the subsequences finishes and will return
-                the first result.
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+                soon as one of the workflows finishes and will return
+                the first result. Defaults to False.
         """
-        if not is_sequence(pass_sequences):
-            raise TypeError(
-                'Expected sequence of sequences of passes, got %s.'
-                % type(pass_sequences),
-            )
-
-        self.pass_seqs = []
-        for ps in pass_sequences:
-            if not is_sequence(ps) and not isinstance(ps, BasePass):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(ps),
-                )
-
-            if is_sequence(ps):
-                truth_list = [isinstance(elem, BasePass) for elem in ps]
-                if not all(truth_list):
-                    raise TypeError(
-                        'Expected Pass or sequence of Passes, got %s.'
-                        % type(ps[truth_list.index(False)]),
-                    )
-                if len(ps) == 0:
-                    raise ValueError('Expected at least one pass.')
-
-            self.pass_seqs.append(ps if is_sequence(ps) else [ps])
+        if not is_iterable(pass_sequences):
+            bad_type = type(pass_sequences)
+            raise TypeError(f'Expected sequence of workflows, got {bad_type}.')
+
+        if not callable(less_than):
+            bad_type = type(less_than)
+            msg = f'Expected callable function for less_than, got {bad_type}'
+            raise TypeError(msg)
 
+        self.workflows = [Workflow(p) for p in pass_sequences]
         self.less_than = less_than
         self.pick_first = pick_fisrt
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        if len(self.workflows) == 0:
+            raise ValueError('Must specify at least one workflow.')
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug('Running pass sequences in parallel.')
 
-        if 'parallel' not in data:
-            raise RuntimeError(
-                'Parallel pass cannot be executed without a compiler.',
-            )
-
-        with worker_client() as client:
-            futures = []
-            for pass_seq in self.pass_seqs:
-                future = client.submit(_sub_do_work, pass_seq, circuit, data)
-                futures.append(future)
-
-            if self.pick_first:
-                _, result = next(as_completed(futures, with_results=True))
-                for future in futures:
-                    if not future.done:
-                        client.cancel(future)
-
-                circuit.become(result[0])
-                data.update(result[1])
-
-            else:
-                best_circ = None
-                best_data = None
-                for future in futures:
-                    _circ, _data = future.result()
-                    if best_circ is None or self.less_than(_circ, best_circ):
-                        best_circ = _circ
-                        best_data = _data
-                if best_circ is None or best_data is None:
-                    raise RuntimeError('No valid circuit found.')
-                circuit.become(best_circ)
-                data.update(best_data)
-
-
-def _sub_do_work(
-    loop_body: Sequence[BasePass],
-    circuit: Circuit,
-    data: dict[str, Any],
-) -> tuple[Circuit, dict[str, Any]]:
-    for loop_pass in loop_body:
-        loop_pass.run(circuit, data)
-    return circuit, data
+        # Submit jobs to the runtime
+        runtime = get_runtime()
+        future = runtime.map(
+            _sub_do_work,
+            self.workflows,
+            circuit=circuit,
+            data=data,
+        )
+
+        # Wait for results
+        if self.pick_first:
+            circuits_and_ids = await runtime.next(future)  # Wake on next result
+            circuits = [x[1] for x in circuits_and_ids]
+            runtime.cancel(future)  # Cancel remaining
+        else:
+            circuits = await future
+
+        # Find the best result
+        best_circ, best_data = circuits[0]
+        for _circ, _data in circuits[1:]:
+            if self.less_than(_circ, best_circ):
+                best_circ = _circ
+                best_data = _data
+
+        # Become best result
+        circuit.become(best_circ)
+        data.become(best_data)
diff --git a/bqskit/passes/control/predicate.py b/bqskit/passes/control/predicate.py
index 9a04be925..9e8854e0a 100644
--- a/bqskit/passes/control/predicate.py
+++ b/bqskit/passes/control/predicate.py
@@ -2,8 +2,8 @@
 from __future__ import annotations
 
 import abc
-from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -16,16 +16,16 @@ class PassPredicate(abc.ABC):
     """
 
     @abc.abstractmethod
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate and retrieve the truth value result."""
 
-    def __call__(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def __call__(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate and retrieve the truth value result."""
 
         if not isinstance(circuit, Circuit):
-            raise TypeError('Expected Circuit, got %s.' % type(circuit))
+            raise TypeError(f'Expected Circuit, got {type(circuit)}.')
 
-        if not isinstance(data, dict):
-            raise TypeError('Expected dictionary, got %s.' % type(data))
+        if not isinstance(data, PassData):
+            raise TypeError(f'Expected PassData, got {type(data)}.')
 
         return self.get_truth_value(circuit, data)
diff --git a/bqskit/passes/control/predicates/__init__.py b/bqskit/passes/control/predicates/__init__.py
index 8438a7e9f..2037cb5c3 100644
--- a/bqskit/passes/control/predicates/__init__.py
+++ b/bqskit/passes/control/predicates/__init__.py
@@ -1,10 +1,12 @@
 """This package implements predicates for use in control passes."""
 from __future__ import annotations
 
+from bqskit.passes.control.predicates.andpredicate import AndPredicate
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
 from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
+from bqskit.passes.control.predicates.orpredicate import OrPredicate
 from bqskit.passes.control.predicates.physical import PhysicalPredicate
 from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
@@ -12,7 +14,9 @@
 __all__ = [
     'ChangePredicate',
     'GateCountPredicate',
+    'AndPredicate',
     'NotPredicate',
+    'OrPredicate',
     'WidthPredicate',
     'PhysicalPredicate',
     'SinglePhysicalPredicate',
diff --git a/bqskit/passes/control/predicates/andpredicate.py b/bqskit/passes/control/predicates/andpredicate.py
new file mode 100644
index 000000000..fdd4bef80
--- /dev/null
+++ b/bqskit/passes/control/predicates/andpredicate.py
@@ -0,0 +1,41 @@
+"""This module implements the AndPredicate class."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from bqskit.passes.control.predicate import PassPredicate
+
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+
+class AndPredicate(PassPredicate):
+    """
+    The AndPredicate class.
+
+    The AndPredicate takes two predicates and returns true iff both are true.
+    """
+
+    def __init__(self, p1: PassPredicate, p2: PassPredicate) -> None:
+        """
+        Construct a AndPredicate.
+
+        Args:
+            p1 (PassPredicate): The first predicate.
+
+            p2 (PassPredicate): The second predicate.
+        """
+
+        if not isinstance(p1, PassPredicate):
+            raise TypeError(f'Expected PassPredicate, got {type(p1)}.')
+
+        if not isinstance(p2, PassPredicate):
+            raise TypeError(f'Expected PassPredicate, got {type(p2)}.')
+
+        self.p1 = p1
+        self.p2 = p2
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return self.p1(circuit, data) and self.p2(circuit, data)
diff --git a/bqskit/passes/control/predicates/change.py b/bqskit/passes/control/predicates/change.py
index 1df3fc0a4..8bf5a2ed2 100644
--- a/bqskit/passes/control/predicates/change.py
+++ b/bqskit/passes/control/predicates/change.py
@@ -2,11 +2,15 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+
 _logger = logging.getLogger(__name__)
 
 
@@ -20,7 +24,7 @@ class ChangePredicate(PassPredicate):
 
     key = 'ChangePredicate_circuit_hash'
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
 
         # If first call, record data and return true
@@ -32,6 +36,7 @@ def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
         # Otherwise, check for a change and return accordingly
         new_hash = self.get_hash(circuit)
         if data[self.key] == new_hash:
+            # TODO: hash collisions?
             _logger.debug('Hashes match; no change detected.')
             return False
 
diff --git a/bqskit/passes/control/predicates/count.py b/bqskit/passes/control/predicates/count.py
index bc19b6bdf..c0dfaf37c 100644
--- a/bqskit/passes/control/predicates/count.py
+++ b/bqskit/passes/control/predicates/count.py
@@ -2,14 +2,19 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
-from typing import Sequence
+from typing import TYPE_CHECKING
 
-from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.passes.control.predicate import PassPredicate
 from bqskit.utils.typing import is_sequence
 
+if TYPE_CHECKING:
+    from typing import Sequence
+
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+
 _logger = logging.getLogger(__name__)
 
 
@@ -17,8 +22,8 @@ class GateCountPredicate(PassPredicate):
     """
     The GateCountPredicate class.
 
-    The GateCountPredicate returns true if the number of one type of gate has
-    changed in the circuit.
+    The GateCountPredicate returns true if the number of one of more types of
+    gate has changed in the circuit.
     """
 
     key = 'GateCountPredicate_circuit_count'
@@ -34,7 +39,7 @@ def __init__(self, gate: Gate | Sequence[Gate]) -> None:
 
         self.gate = gate
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
         gate_count = sum(circuit.count(g) for g in self.gate)
 
diff --git a/bqskit/passes/control/predicates/multi.py b/bqskit/passes/control/predicates/multi.py
index c841b07e3..8d640f6db 100644
--- a/bqskit/passes/control/predicates/multi.py
+++ b/bqskit/passes/control/predicates/multi.py
@@ -2,12 +2,14 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
 _logger = logging.getLogger(__name__)
 
 
@@ -19,9 +21,9 @@ class MultiPhysicalPredicate(PassPredicate):
     in the native gate set.
     """
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        model = BasePass.get_model(circuit, data)
+        model = data.model
         for gate in circuit.gate_set:
             if gate.num_qudits < 2:
                 continue
diff --git a/bqskit/passes/control/predicates/notpredicate.py b/bqskit/passes/control/predicates/notpredicate.py
index 3164ea9dd..00f8a7560 100644
--- a/bqskit/passes/control/predicates/notpredicate.py
+++ b/bqskit/passes/control/predicates/notpredicate.py
@@ -1,13 +1,13 @@
 """This module implements the NotPredicate class."""
 from __future__ import annotations
 
-import logging
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 
-_logger = logging.getLogger(__name__)
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
 
 
 class NotPredicate(PassPredicate):
@@ -27,10 +27,10 @@ def __init__(self, predicate: PassPredicate) -> None:
         """
 
         if not isinstance(predicate, PassPredicate):
-            raise TypeError('Expected PassPredicate, got %s.' % type(predicate))
+            raise TypeError(f'Expected PassPredicate, got {type(predicate)}.')
 
         self.predicate = predicate
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
         return not self.predicate(circuit, data)
diff --git a/bqskit/passes/control/predicates/orpredicate.py b/bqskit/passes/control/predicates/orpredicate.py
new file mode 100644
index 000000000..2c15de16a
--- /dev/null
+++ b/bqskit/passes/control/predicates/orpredicate.py
@@ -0,0 +1,41 @@
+"""This module implements the OrPredicate class."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from bqskit.passes.control.predicate import PassPredicate
+
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+
+class OrPredicate(PassPredicate):
+    """
+    The OrPredicate class.
+
+    The OrPredicate takes two predicates and returns true if either are true.
+    """
+
+    def __init__(self, p1: PassPredicate, p2: PassPredicate) -> None:
+        """
+        Construct a OrPredicate.
+
+        Args:
+            p1 (PassPredicate): The first predicate.
+
+            p2 (PassPredicate): The second predicate.
+        """
+
+        if not isinstance(p1, PassPredicate):
+            raise TypeError(f'Expected PassPredicate, got {type(p1)}.')
+
+        if not isinstance(p2, PassPredicate):
+            raise TypeError(f'Expected PassPredicate, got {type(p2)}.')
+
+        self.p1 = p1
+        self.p2 = p2
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return self.p1(circuit, data) or self.p2(circuit, data)
diff --git a/bqskit/passes/control/predicates/physical.py b/bqskit/passes/control/predicates/physical.py
index fe62eb5d5..26440025b 100644
--- a/bqskit/passes/control/predicates/physical.py
+++ b/bqskit/passes/control/predicates/physical.py
@@ -1,44 +1,23 @@
 """This module implements the PhysicalPredicate class."""
 from __future__ import annotations
 
-import logging
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 
-_logger = logging.getLogger(__name__)
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
 
 
 class PhysicalPredicate(PassPredicate):
     """
     The PhysicalPredicate class.
 
-    The PhysicalPredicate returns true if circuit's coupling graph and gate set
-    matches the workflow's machine model.
+    The PhysicalPredicate returns true if the circuit can be executed on the
+    workflow's machine model with the current placement.
     """
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        model = BasePass.get_model(circuit, data)
-        for gate in circuit.gate_set:
-            if gate not in model.gate_set:
-                _logger.debug(f'{gate} not in native set: {model.gate_set}.')
-                return False
-
-        if circuit.num_qudits == 1:
-            return True
-
-        try:
-            placement = BasePass.get_placement(circuit, data)
-            subgraph = model.coupling_graph.get_subgraph(placement)
-            if not subgraph.is_fully_connected():
-                _logger.debug('Qudits are disconnected on the machine model.')
-                return False
-
-        except RuntimeError:
-            _logger.debug('Qudits are disconnected on the machine model.')
-            return False
-
-        return True
+        return data.model.is_compatible(circuit, data.placement)
diff --git a/bqskit/passes/control/predicates/single.py b/bqskit/passes/control/predicates/single.py
index 0c38af083..c935862e2 100644
--- a/bqskit/passes/control/predicates/single.py
+++ b/bqskit/passes/control/predicates/single.py
@@ -1,13 +1,15 @@
-"""This module implements the PhysicalPredicate class."""
+"""This module implements the SinglePhysicalPredicate class."""
 from __future__ import annotations
 
 import logging
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
 _logger = logging.getLogger(__name__)
 
 
@@ -19,9 +21,9 @@ class SinglePhysicalPredicate(PassPredicate):
     in the native gate set.
     """
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        model = BasePass.get_model(circuit, data)
+        model = data.model
         for gate in circuit.gate_set:
             if gate.num_qudits > 1:
                 continue
diff --git a/bqskit/passes/control/predicates/width.py b/bqskit/passes/control/predicates/width.py
index 0bcb12d0c..4d9b7c9bb 100644
--- a/bqskit/passes/control/predicates/width.py
+++ b/bqskit/passes/control/predicates/width.py
@@ -1,19 +1,22 @@
 """This module implements the WidthPredicate class."""
 from __future__ import annotations
 
-from typing import Any
+from typing import TYPE_CHECKING
 
-from bqskit.ir.circuit import Circuit
 from bqskit.passes.control.predicate import PassPredicate
 from bqskit.utils.typing import is_integer
 
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
 
 class WidthPredicate(PassPredicate):
     """
     The WidthPredicate class.
 
-    The WidthPredicate class returns True if the circuit's width is less than a
-    specified number.
+    The WidthPredicate class returns True if the circuit's width (number of
+    qudits) is less than a specified number.
     """
 
     def __init__(self, width: int) -> None:
@@ -27,8 +30,8 @@ def __init__(self, width: int) -> None:
         if not is_integer(width):
             raise TypeError(f'Expected int, got {type(width)}')
 
-        self.width = width
+        self.width = int(width)
 
-    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
         return circuit.num_qudits < self.width
diff --git a/bqskit/passes/control/whileloop.py b/bqskit/passes/control/whileloop.py
index 72057d02a..211a8524c 100644
--- a/bqskit/passes/control/whileloop.py
+++ b/bqskit/passes/control/whileloop.py
@@ -2,13 +2,16 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
-from typing import Sequence
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
+from bqskit.compiler.workflow import Workflow
 from bqskit.passes.control.predicate import PassPredicate
-from bqskit.utils.typing import is_sequence
+
+if TYPE_CHECKING:
+    from bqskit.ir.circuit import Circuit
+    from bqskit.compiler.passdata import PassData
+    from bqskit.compiler.workflow import WorkflowLike
 
 _logger = logging.getLogger(__name__)
 
@@ -17,13 +20,13 @@ class WhileLoopPass(BasePass):
     """
     The WhileLoopPass class.
 
-    This is a control pass that conditionally executes another pass in a loop.
+    This is a control pass that conditionally executes a workflow in a loop.
     """
 
     def __init__(
         self,
         condition: PassPredicate,
-        loop_body: BasePass | Sequence[BasePass],
+        loop_body: WorkflowLike,
     ) -> None:
         """
         Construct a WhileLoopPass.
@@ -31,38 +34,17 @@ def __init__(
         Args:
             condition (PassPredicate): The condition checked.
 
-            loop_body (BasePass | Sequence[BasePass]): The pass or passes
-                to execute while `condition` is true.
-
-        Raises:
-            ValueError: If a Sequence[BasePass] is given, but it is empty.
+            loop_body (WorkflowLike): The pass or passes to execute
+                while `condition` is true.
         """
-
         if not isinstance(condition, PassPredicate):
-            raise TypeError('Expected PassPredicate, got %s.' % type(condition))
-
-        if not is_sequence(loop_body) and not isinstance(loop_body, BasePass):
-            raise TypeError(
-                'Expected Pass or sequence of Passes, got %s.'
-                % type(loop_body),
-            )
-
-        if is_sequence(loop_body):
-            truth_list = [isinstance(elem, BasePass) for elem in loop_body]
-            if not all(truth_list):
-                raise TypeError(
-                    'Expected Pass or sequence of Passes, got %s.'
-                    % type(loop_body[truth_list.index(False)]),
-                )
-            if len(loop_body) == 0:
-                raise ValueError('Expected at least one pass.')
+            raise TypeError(f'Expected PassPredicate, got {type(condition)}.')
 
         self.condition = condition
-        self.loop_body = loop_body if is_sequence(loop_body) else [loop_body]
+        self.workflow = Workflow(loop_body)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         while self.condition(circuit, data):
             _logger.debug('Loop body executing...')
-            for loop_pass in self.loop_body:
-                loop_pass.run(circuit, data)
+            await self.workflow.run(circuit, data)
diff --git a/bqskit/passes/io/checkpoint.py b/bqskit/passes/io/checkpoint.py
index f9631b66f..7a7dbd776 100644
--- a/bqskit/passes/io/checkpoint.py
+++ b/bqskit/passes/io/checkpoint.py
@@ -3,9 +3,9 @@
 
 import logging
 import pickle
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ def __init__(self, checkpoint_filename: str) -> None:
         """
         self.checkpoint_filename = checkpoint_filename
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see BasePass for more info."""
         with open(self.checkpoint_filename, 'wb') as f:
             pickle.dump((circuit, data), f)
@@ -49,7 +49,7 @@ def __init__(self, checkpoint_filename: str) -> None:
         """
         self.checkpoint_filename = checkpoint_filename
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see BasePass for more info."""
         with open(self.checkpoint_filename, 'rb') as f:
             checkpoint = pickle.load(f)
diff --git a/bqskit/passes/io/intermediate.py b/bqskit/passes/io/intermediate.py
index 2df59b6bc..47c85b9f1 100644
--- a/bqskit/passes/io/intermediate.py
+++ b/bqskit/passes/io/intermediate.py
@@ -7,9 +7,9 @@
 from os import mkdir
 from os.path import exists
 from re import findall
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.lang.qasm2.qasm2 import OPENQASM2Language
@@ -71,7 +71,7 @@ def __init__(
 
         self.as_qasm = save_as_qasm
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see BasePass for more info."""
 
         # Gather and enumerate CircuitGates to save
@@ -102,7 +102,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 block.params,
             )
             subcircuit.unfold((0, 0))
-            ToU3Pass().run(subcircuit, {})
+            await ToU3Pass().run(subcircuit, PassData(subcircuit))
             if self.as_qasm:
                 with open(block_skeleton + f'{enum}.qasm', 'w') as f:
                     f.write(OPENQASM2Language().encode(subcircuit))
@@ -171,7 +171,7 @@ def reload_blocks(self) -> None:
                 'More block files than indicies in `structure.pickle`',
             )
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """
         Perform the pass's operation, see BasePass for more info.
 
diff --git a/bqskit/passes/mapping/apply.py b/bqskit/passes/mapping/apply.py
index 060786778..1dc637986 100644
--- a/bqskit/passes/mapping/apply.py
+++ b/bqskit/passes/mapping/apply.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
@@ -13,14 +13,14 @@
 class ApplyPlacement(BasePass):
     """Place the circuit on the machine model."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        model = self.get_model(circuit, data)
-        placement = self.get_placement(circuit, data)
+        model = data.model
+        placement = data.placement
         physical_circuit = Circuit(model.num_qudits, model.radixes)
         physical_circuit.append_circuit(circuit, placement)
         circuit.become(physical_circuit)
         if 'final_mapping' in data:
             pi = data['final_mapping']
             data['final_mapping'] = [placement[p] for p in pi]
-        data['placement'] = list(i for i in range(model.num_qudits))
+        data.placement = list(i for i in range(model.num_qudits))
diff --git a/bqskit/passes/mapping/layout/sabre.py b/bqskit/passes/mapping/layout/sabre.py
index c2587bc8a..7cecca197 100644
--- a/bqskit/passes/mapping/layout/sabre.py
+++ b/bqskit/passes/mapping/layout/sabre.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.mapping.sabre import GeneralizedSabreAlgorithm
 
@@ -67,7 +67,7 @@ def __init__(
             extended_set_weight,
         )
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         subgraph = self.get_connectivity(circuit, data)
         if not subgraph.is_fully_connected():
diff --git a/bqskit/passes/mapping/placement/greedy.py b/bqskit/passes/mapping/placement/greedy.py
index 7e91a13e6..ac8a58879 100644
--- a/bqskit/passes/mapping/placement/greedy.py
+++ b/bqskit/passes/mapping/placement/greedy.py
@@ -2,11 +2,11 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 _logger = logging.getLogger(__name__)
 
@@ -14,7 +14,7 @@
 class GreedyPlacementPass(BasePass):
     """Find a placement by starting with the most connected qudit."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find physical qudit with highest degree
diff --git a/bqskit/passes/mapping/placement/trivial.py b/bqskit/passes/mapping/placement/trivial.py
index cbea3fb9e..df54d94e5 100644
--- a/bqskit/passes/mapping/placement/trivial.py
+++ b/bqskit/passes/mapping/placement/trivial.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 _logger = logging.getLogger(__name__)
 
@@ -12,7 +12,7 @@
 class TrivialPlacementPass(BasePass):
     """Place the logical qubits on the first n physical qubits."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Select the first n physical qubits for placement
diff --git a/bqskit/passes/mapping/routing/sabre.py b/bqskit/passes/mapping/routing/sabre.py
index bb930b6d3..3179a033e 100644
--- a/bqskit/passes/mapping/routing/sabre.py
+++ b/bqskit/passes/mapping/routing/sabre.py
@@ -3,9 +3,9 @@
 
 import copy
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.mapping.sabre import GeneralizedSabreAlgorithm
 
@@ -20,7 +20,7 @@ class GeneralizedSabreRoutingPass(BasePass, GeneralizedSabreAlgorithm):
     See :class:`GeneralizedSabreAlgorithm` for more info.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         subgraph = self.get_connectivity(circuit, data)
         if not subgraph.is_fully_connected():
diff --git a/bqskit/passes/mapping/setmodel.py b/bqskit/passes/mapping/setmodel.py
index 36e4435cd..95294df47 100644
--- a/bqskit/passes/mapping/setmodel.py
+++ b/bqskit/passes/mapping/setmodel.py
@@ -2,10 +2,10 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
@@ -27,12 +27,10 @@ def __init__(self, model: MachineModel) -> None:
 
         self.model = model
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        if 'machine_model' in data:
-            _logger.info('Overriding existing machine model.')
-
         if self.model.num_qudits < circuit.num_qudits:
             raise RuntimeError('Machine model is too small for circuit.')
 
-        data['machine_model'] = self.model
+        data.model = self.model  # Update Model
+        data.placement = list(range(circuit.num_qudits))  # Reset placement
diff --git a/bqskit/passes/measure.py b/bqskit/passes/measure.py
index d0a218fe7..9d2dd9869 100644
--- a/bqskit/passes/measure.py
+++ b/bqskit/passes/measure.py
@@ -3,9 +3,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.point import CircuitPoint
@@ -17,7 +17,7 @@ class ExtractMeasurements(BasePass):
 
     key = '__measurement_data__'
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.info('Extracting measurements from circuit.')
         points_to_remove: list[CircuitPoint] = []
@@ -41,7 +41,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 class RestoreMeasurements(BasePass):
     """A pass that restores the measurements in a circuit."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.info('Restoring measurements to circuit.')
         if ExtractMeasurements.key in data:
diff --git a/bqskit/passes/noop.py b/bqskit/passes/noop.py
index 9d946ef5d..1d8c1d764 100644
--- a/bqskit/passes/noop.py
+++ b/bqskit/passes/noop.py
@@ -1,15 +1,14 @@
 """This module implements the NOOPPass class."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
 class NOOPPass(BasePass):
     """A pass that does not perform any operation."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         return
diff --git a/bqskit/passes/partitioning/cluster.py b/bqskit/passes/partitioning/cluster.py
index 8e3c7f193..1fc0ca0c3 100644
--- a/bqskit/passes/partitioning/cluster.py
+++ b/bqskit/passes/partitioning/cluster.py
@@ -2,11 +2,11 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.utils.typing import is_integer
 
@@ -61,7 +61,7 @@ def __init__(self, block_size: int = 3, num_points: int = 8) -> None:
         self.block_size = block_size
         self.num_points = num_points
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         if self.block_size > circuit.num_qudits:
diff --git a/bqskit/passes/partitioning/greedy.py b/bqskit/passes/partitioning/greedy.py
index 3ede6b2f4..689c9196f 100644
--- a/bqskit/passes/partitioning/greedy.py
+++ b/bqskit/passes/partitioning/greedy.py
@@ -4,9 +4,9 @@
 import bisect
 import logging
 import warnings
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.interval import CycleInterval
@@ -59,7 +59,7 @@ def __init__(self, block_size: int = 3) -> None:
 
         self.block_size = block_size
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         if self.block_size > circuit.num_qudits:
diff --git a/bqskit/passes/partitioning/quick.py b/bqskit/passes/partitioning/quick.py
index 537133989..8fa505562 100644
--- a/bqskit/passes/partitioning/quick.py
+++ b/bqskit/passes/partitioning/quick.py
@@ -2,11 +2,11 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import cast
 from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.location import CircuitLocation
@@ -48,7 +48,7 @@ def __init__(self, block_size: int = 3) -> None:
 
         self.block_size = block_size
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """
         Partition gates in a circuit into a series of CircuitGates.
 
diff --git a/bqskit/passes/partitioning/scan.py b/bqskit/passes/partitioning/scan.py
index c7486502e..e760ede42 100644
--- a/bqskit/passes/partitioning/scan.py
+++ b/bqskit/passes/partitioning/scan.py
@@ -2,7 +2,6 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import Callable
 from typing import Iterator
 from typing import Sequence
@@ -10,6 +9,7 @@
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.operation import Operation
@@ -73,7 +73,7 @@ def __init__(
         self.block_size = block_size
         self.scoring_fn = scoring_fn
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         if self.block_size > circuit.num_qudits:
diff --git a/bqskit/passes/partitioning/single.py b/bqskit/passes/partitioning/single.py
index a59645c36..d73f7abe7 100644
--- a/bqskit/passes/partitioning/single.py
+++ b/bqskit/passes/partitioning/single.py
@@ -1,9 +1,8 @@
 """This module implements the GroupSingleQuditGatePass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.region import CircuitRegion
 
@@ -15,7 +14,7 @@ class GroupSingleQuditGatePass(BasePass):
     This pass groups together consecutive single-qudit gates.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Go through each qudit individually
diff --git a/bqskit/passes/processing/__init__.py b/bqskit/passes/processing/__init__.py
index 5cd7c4104..2d7712107 100644
--- a/bqskit/passes/processing/__init__.py
+++ b/bqskit/passes/processing/__init__.py
@@ -6,13 +6,11 @@
 from bqskit.passes.processing.rebase import Rebase2QuditGatePass
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
 from bqskit.passes.processing.substitute import SubstitutePass
-from bqskit.passes.processing.window import WindowOptimizationPass
 
 __all__ = [
     'ExhaustiveGateRemovalPass',
     'IterativeScanningGateRemovalPass',
     'ScanningGateRemovalPass',
     'SubstitutePass',
-    'WindowOptimizationPass',
     'Rebase2QuditGatePass',
 ]
diff --git a/bqskit/passes/processing/exhaustive.py b/bqskit/passes/processing/exhaustive.py
index f2cb7aa0c..64656cf67 100644
--- a/bqskit/passes/processing/exhaustive.py
+++ b/bqskit/passes/processing/exhaustive.py
@@ -8,11 +8,13 @@
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.operation import Operation
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.ir.structure import CircuitStructure
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_real_number
 _logger = logging.getLogger(__name__)
 
@@ -104,8 +106,12 @@ def __init__(
         }
         self.instantiate_options.update(instantiate_options)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
+
         _logger.debug('Starting exhaustive gate removal.')
 
         target = self.get_target(circuit, data)
@@ -137,12 +143,11 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                         circuits_seen.add(structure)
 
             # Instantiate them all
-            instantiated_circuits = self.execute(
-                data,
+            instantiated_circuits = await get_runtime().map(
                 Circuit.instantiate,
                 expanded_circuits,
                 target=target,
-                **self.instantiate_options,
+                **instantiate_options,
             )
 
             # Process them
diff --git a/bqskit/passes/processing/rebase.py b/bqskit/passes/processing/rebase.py
index 1df832182..b2768c473 100644
--- a/bqskit/passes/processing/rebase.py
+++ b/bqskit/passes/processing/rebase.py
@@ -6,12 +6,14 @@
 from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir import Gate
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import U3Gate
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.ir.point import CircuitPoint as Point
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_real_number
 from bqskit.utils.typing import is_sequence
@@ -151,8 +153,11 @@ def __init__(
         self.sq = single_qudit_gate
         self.generate_new_gate_templates()
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
         _logger.debug(f'Rebasing gates from {self.gates} to {self.ngates}.')
 
         target = self.get_target(circuit, data)
@@ -186,12 +191,11 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                     circuit_copy.replace_with_circuit(point, self.overdrive)
                     circuits_with_new_gate.append(circuit_copy)
 
-                instantiated_circuits = self.execute(
-                    data,
+                instantiated_circuits = await get_runtime().map(
                     Circuit.instantiate,
                     circuits_with_new_gate,
                     target=target,
-                    **self.instantiate_options,
+                    **instantiate_options,
                 )
 
                 dists = [self.cost(c, target) for c in instantiated_circuits]
@@ -277,7 +281,7 @@ def generate_new_gate_templates(self) -> None:
         self.circs = []
         self.counts = []
 
-        circ = Circuit(2)
+        circ = Circuit(2, self.ngates[0].radixes)
         circ.append_gate(self.sq, 0)
         circ.append_gate(self.sq, 1)
         self.circs.append(circ)
@@ -285,7 +289,7 @@ def generate_new_gate_templates(self) -> None:
 
         for g in self.ngates:
             for i in range(1, self.max_depth + 1):
-                circ = Circuit(2)
+                circ = Circuit(2, self.ngates[0].radixes)
                 circ.append_gate(self.sq, 0)
                 circ.append_gate(self.sq, 1)
 
diff --git a/bqskit/passes/processing/scan.py b/bqskit/passes/processing/scan.py
index 61cc89bc6..3ae7d589d 100644
--- a/bqskit/passes/processing/scan.py
+++ b/bqskit/passes/processing/scan.py
@@ -6,10 +6,12 @@
 from typing import Callable
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.operation import Operation
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_real_number
 _logger = logging.getLogger(__name__)
 
@@ -90,11 +92,16 @@ def __init__(
         self.instantiate_options: dict[str, Any] = {
             'dist_tol': self.success_threshold,
             'min_iters': 100,
+            'cost_fn_gen': self.cost,
         }
         self.instantiate_options.update(instantiate_options)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
+
         start = 'left' if self.start_from_left else 'right'
         _logger.debug(f'Starting scanning gate removal on the {start}.')
 
@@ -120,13 +127,12 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 cycle -= idx_shift
 
             working_copy.pop((cycle, op.location[0]))
-            working_copy = self.execute(
-                data,
+            working_copy = await get_runtime().submit(
                 Circuit.instantiate,
-                [working_copy],
+                working_copy,
                 target=target,
-                **self.instantiate_options,
-            )[0]
+                **instantiate_options,
+            )
 
             if self.cost(working_copy, target) < self.success_threshold:
                 _logger.debug('Successfully removed operation.')
diff --git a/bqskit/passes/processing/substitute.py b/bqskit/passes/processing/substitute.py
index dbce50344..37dc5f057 100644
--- a/bqskit/passes/processing/substitute.py
+++ b/bqskit/passes/processing/substitute.py
@@ -7,11 +7,13 @@
 from typing import Callable
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.operation import Operation
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_real_number
 _logger = logging.getLogger(__name__)
 
@@ -91,8 +93,11 @@ def __init__(
         }
         self.instantiate_options.update(instantiate_options)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
 
         # Collect locations in circuit where self.init_gate exists
         points = [
@@ -125,13 +130,12 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 _logger.debug(f'Trying location: {loc}')
                 circuit_copy = circuit.copy()
                 circuit_copy.replace_gate(point, self.gate, loc)
-                circuit_copy = self.execute(
-                    data,
+                circuit_copy = await get_runtime().submit(
                     Circuit.instantiate,
-                    [circuit_copy],
+                    circuit_copy,
                     target=target,
-                    **self.instantiate_options,
-                )[0]
+                    **instantiate_options,
+                )
 
                 if self.cost(circuit_copy, target) < self.success_threshold:
                     _logger.info('Successfully substituted operation.')
diff --git a/bqskit/passes/processing/window.py b/bqskit/passes/processing/window.py
deleted file mode 100644
index 43efac627..000000000
--- a/bqskit/passes/processing/window.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""This module implements the WindowOptimizationPass."""
-from __future__ import annotations
-
-import logging
-import warnings
-from typing import Any
-from typing import Callable
-
-import numpy as np
-
-from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
-from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass
-from bqskit.passes.synthesis.synthesis import SynthesisPass
-from bqskit.utils.typing import is_integer
-from bqskit.utils.typing import is_iterable
-
-_logger = logging.getLogger(__name__)
-
-
-class WindowOptimizationPass(BasePass):
-    """
-    The WindowOptimizationPass class.
-
-    Previously run passes can leave window markers. This pass will select a
-    window around that marker and resynthesize, potentially for a depth
-    reduction.
-    """
-
-    def __init__(
-        self,
-        window_size: int = 9,
-        synthesispass: SynthesisPass = QSearchSynthesisPass(),
-        replace_filter: Callable[[Circuit, Circuit], bool] | None = None,
-    ) -> None:
-        """
-        Construct a WindowOptimizationPass.
-
-        Args:
-            window_size (int): The size of the window to surround each
-                marker. (Default: 9)
-
-            synthesispass (SynthesisPass): The configured synthesis algorithm
-                to use during window synthesis.
-
-            replace_filter (Callable[[Circuit, Circuit], bool] | None):
-                A predicate that determines if the synthesis result should
-                replace the original operation. The first parameter is the
-                output from synthesis and the second parameter is the original
-                window. If this returns true, the newly synthesized circuit
-                will replace the original circuit. Defaults to always replace.
-
-        Raises:
-            ValueError: If `window_size` is <= 1.
-
-        Note:
-            This has been deprecated and will removed in a future update.
-            See :class:`ScanningGateRemovalPass` for a replacement
-            post-synthesis optimization pass.
-        """
-        warnings.warn(
-            'WindowOptimizationPass has been deprecated and will be '
-            'removed in a future update. See ScanningGateRemovalpass '
-            'for a replacement post-synthesis optimization pass.',
-            DeprecationWarning,
-        )
-
-        if not is_integer(window_size):
-            raise TypeError(
-                'Expected integer for success_threshold'
-                ', got %s' % type(window_size),
-            )
-
-        if window_size <= 1:
-            raise ValueError(
-                'Expected integer greater than 1 for window_size'
-                ', got %d.' % window_size,
-            )
-
-        if not isinstance(synthesispass, SynthesisPass):
-            raise TypeError(
-                'Expected SynthesisPass, got %s.' % type(synthesispass),
-            )
-
-        self.replace_filter = replace_filter or default_replace_filter
-
-        if not callable(self.replace_filter):
-            raise TypeError(
-                'Expected callable method for replace_filter'
-                ', got %s.' % type(self.replace_filter),
-            )
-
-        self.window_size = window_size
-        self.synthesispass = synthesispass
-
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-        """Perform the pass's operation, see :class:`BasePass` for more."""
-
-        # Check data for windows markers
-        if 'window_markers' not in data:
-            _logger.warning('Did not find any window markers.')
-            return
-
-        window_markers = data['window_markers']
-        _logger.debug('Found window_markers: %s.' % str(window_markers))
-
-        if not is_iterable(window_markers):
-            _logger.debug('Invalid type for window_markers.')
-            return
-
-        if not all(is_integer(marker) for marker in window_markers):
-            _logger.debug('Invalid type for window_markers.')
-            return
-
-        # Resynthesis each window
-        index_shift = 0
-        for marker in window_markers:
-            marker -= index_shift
-
-            # Slice window
-            begin_cycle = int(marker - self.window_size // 2)
-            end_cycle = int(marker + np.ceil(self.window_size / 2))
-
-            if begin_cycle < 0:
-                begin_cycle = 0
-
-            if end_cycle > circuit.num_cycles:
-                end_cycle = circuit.num_cycles - 1
-
-            window = Circuit(circuit.num_qudits, circuit.radixes)
-            window.extend(circuit[begin_cycle:end_cycle])
-
-            _logger.info(
-                'Resynthesizing window from cycle '
-                f'{begin_cycle} to {end_cycle}.',
-            )
-
-            # Synthesize
-            utry = window.get_unitary()
-            new_window = self.synthesispass.synthesize(utry, data)
-
-            # Replace
-            if self.replace_filter(new_window, window):
-                _logger.debug('Replacing window with synthesized circuit.')
-
-                actual_window_size = end_cycle - begin_cycle
-                for _ in range(actual_window_size):
-                    circuit.pop_cycle(begin_cycle)
-
-                circuit.insert_circuit(
-                    begin_cycle,
-                    new_window,
-                    list(range(circuit.num_qudits)),
-                )
-
-                index_shift += actual_window_size - new_window.num_cycles
-
-
-def default_replace_filter(new_circuit: Circuit, og_window: Circuit) -> bool:
-    return True
diff --git a/bqskit/passes/rules/ch2cnot.py b/bqskit/passes/rules/ch2cnot.py
index 2daf582b6..ddc46c030 100644
--- a/bqskit/passes/rules/ch2cnot.py
+++ b/bqskit/passes/rules/ch2cnot.py
@@ -1,11 +1,10 @@
 """This module implements the CHToCNOTPass."""
 from __future__ import annotations
 
-from typing import Any
-
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CHGate
 from bqskit.ir.gates import CircuitGate
@@ -29,7 +28,7 @@ def __init__(self) -> None:
         circuit.append_gate(RYGate(), 1, [-np.pi / 4])
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/cnot2ch.py b/bqskit/passes/rules/cnot2ch.py
index 89b3974c3..1939fd1e9 100644
--- a/bqskit/passes/rules/cnot2ch.py
+++ b/bqskit/passes/rules/cnot2ch.py
@@ -1,11 +1,10 @@
 """This module implements the CNOTToCHPass."""
 from __future__ import annotations
 
-from typing import Any
-
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CHGate
 from bqskit.ir.gates import CircuitGate
@@ -29,7 +28,7 @@ def __init__(self) -> None:
         circuit.append_gate(RYGate(), 1, [np.pi / 4])
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/cnot2cy.py b/bqskit/passes/rules/cnot2cy.py
index 45f481883..f7c66d696 100644
--- a/bqskit/passes/rules/cnot2cy.py
+++ b/bqskit/passes/rules/cnot2cy.py
@@ -1,9 +1,8 @@
 """This module implements the CNOTToCYPass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
@@ -28,7 +27,7 @@ def __init__(self) -> None:
         circuit.append_gate(SdgGate(), 1)
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/cnot2cz.py b/bqskit/passes/rules/cnot2cz.py
index 106b5ee4b..859d6b840 100644
--- a/bqskit/passes/rules/cnot2cz.py
+++ b/bqskit/passes/rules/cnot2cz.py
@@ -1,9 +1,8 @@
 """This module implements the CNOTToCZPass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
@@ -27,7 +26,7 @@ def __init__(self) -> None:
         circuit.append_gate(HGate(), 1)
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/cy2cnot.py b/bqskit/passes/rules/cy2cnot.py
index 6b9d599a4..2c177d787 100644
--- a/bqskit/passes/rules/cy2cnot.py
+++ b/bqskit/passes/rules/cy2cnot.py
@@ -1,9 +1,8 @@
 """This module implements the CYToCNOTPass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
@@ -28,7 +27,7 @@ def __init__(self) -> None:
         circuit.append_gate(SGate(), 1)
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/cz2cnot.py b/bqskit/passes/rules/cz2cnot.py
index 144a7ac3c..f7cc6ac44 100644
--- a/bqskit/passes/rules/cz2cnot.py
+++ b/bqskit/passes/rules/cz2cnot.py
@@ -1,9 +1,8 @@
 """This module implements the CZToCNOTPass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
@@ -27,7 +26,7 @@ def __init__(self) -> None:
         circuit.append_gate(HGate(), 1)
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/swap2cnot.py b/bqskit/passes/rules/swap2cnot.py
index d44ec3490..9ac3a0dd1 100644
--- a/bqskit/passes/rules/swap2cnot.py
+++ b/bqskit/passes/rules/swap2cnot.py
@@ -1,9 +1,8 @@
 """This module implements the SwapToCNOTPass."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
@@ -26,7 +25,7 @@ def __init__(self) -> None:
         circuit.append_gate(CNOTGate(), (1, 0))
         self.cg = CircuitGate(circuit)
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Find all cnots
diff --git a/bqskit/passes/rules/u3.py b/bqskit/passes/rules/u3.py
index ea2d8173a..c63c1f482 100644
--- a/bqskit/passes/rules/u3.py
+++ b/bqskit/passes/rules/u3.py
@@ -1,9 +1,8 @@
 """This module implements the U3Decomposition."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import U3Gate
 
@@ -15,7 +14,7 @@ class U3Decomposition(BasePass):
     Convert a single-qubit circuit to U3 gate.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         if circuit.num_qudits != 1:
diff --git a/bqskit/passes/rules/zxzxz.py b/bqskit/passes/rules/zxzxz.py
index 9f85495bf..7099bd8ea 100644
--- a/bqskit/passes/rules/zxzxz.py
+++ b/bqskit/passes/rules/zxzxz.py
@@ -2,11 +2,11 @@
 from __future__ import annotations
 
 import cmath
-from typing import Any
 
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import RZGate
 from bqskit.ir.gates import SqrtXGate
@@ -19,7 +19,7 @@ class ZXZXZDecomposition(BasePass):
     Convert a single-qubit circuit to ZXZXZ sequence.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         if circuit.num_qudits != 1:
diff --git a/bqskit/passes/search/frontier.py b/bqskit/passes/search/frontier.py
index e30caa939..629f1666d 100644
--- a/bqskit/passes/search/frontier.py
+++ b/bqskit/passes/search/frontier.py
@@ -9,6 +9,7 @@
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -25,21 +26,21 @@ class Frontier:
 
     def __init__(
         self,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
         heuristic_function: HeuristicFunction,
     ) -> None:
         """
         Construct an empty frontier.
 
         Args:
-            target (UnitaryMatrix | StateVector): The target to pass to
-                the heuristic_function.
+            target (UnitaryMatrix | StateVector | StateSystem): The target to
+                pass to the heuristic_function.
 
             heuristic_function (HeuristicFunction): The heuristic used
                 to sort the Frontier.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
diff --git a/bqskit/passes/search/generator.py b/bqskit/passes/search/generator.py
index 81d1f4eea..e9d081a87 100644
--- a/bqskit/passes/search/generator.py
+++ b/bqskit/passes/search/generator.py
@@ -2,10 +2,11 @@
 from __future__ import annotations
 
 import abc
-from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -20,15 +21,11 @@ class LayerGenerator(abc.ABC):
     @abc.abstractmethod
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """Generate the initial layer for search."""
 
     @abc.abstractmethod
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """Generate the successors of a circuit node."""
diff --git a/bqskit/passes/search/generators/fourparam.py b/bqskit/passes/search/generators/fourparam.py
index b1a36d6b1..e03e88381 100644
--- a/bqskit/passes/search/generators/fourparam.py
+++ b/bqskit/passes/search/generators/fourparam.py
@@ -2,9 +2,8 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
-from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CNOTGate
 from bqskit.ir.gates import RXGate
@@ -13,6 +12,7 @@
 from bqskit.ir.gates import U3Gate
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 _logger = logging.getLogger(__name__)
 
@@ -33,8 +33,8 @@ class FourParamGenerator(LayerGenerator):
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -43,7 +43,7 @@ def gen_initial_layer(
             ValueError: If `target` is not qubit only.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -56,11 +56,7 @@ def gen_initial_layer(
             init_circuit.append_gate(U3Gate(), [i])
         return init_circuit
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
@@ -75,7 +71,7 @@ def gen_successors(
             raise ValueError('Cannot expand a single-qudit circuit.')
 
         # Get the machine model
-        coupling_graph = BasePass.get_connectivity(circuit, data)
+        coupling_graph = data.connectivity
 
         # Generate successors
         successors = []
diff --git a/bqskit/passes/search/generators/middleout.py b/bqskit/passes/search/generators/middleout.py
index ce88fc2c5..56f820ca4 100644
--- a/bqskit/passes/search/generators/middleout.py
+++ b/bqskit/passes/search/generators/middleout.py
@@ -3,9 +3,9 @@
 
 import itertools as it
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.search.generators.seed import SeedLayerGenerator
 from bqskit.passes.search.generators.simple import SimpleLayerGenerator
@@ -23,11 +23,7 @@ def __init__(self) -> None:
         self.generated_circuits = set()
         super().__init__()
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
diff --git a/bqskit/passes/search/generators/seed.py b/bqskit/passes/search/generators/seed.py
index bbc4b99cf..f8cf8fff4 100644
--- a/bqskit/passes/search/generators/seed.py
+++ b/bqskit/passes/search/generators/seed.py
@@ -2,12 +2,13 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.passes.search.generators.simple import SimpleLayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_integer
 _logger = logging.getLogger(__name__)
@@ -53,8 +54,8 @@ def __init__(
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -64,7 +65,7 @@ def gen_initial_layer(
                 `self.seed`.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -76,11 +77,7 @@ def gen_initial_layer(
 
         return self.seed
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
diff --git a/bqskit/passes/search/generators/simple.py b/bqskit/passes/search/generators/simple.py
index 83a062838..218919a2f 100644
--- a/bqskit/passes/search/generators/simple.py
+++ b/bqskit/passes/search/generators/simple.py
@@ -2,15 +2,15 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
-from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import CNOTGate
 from bqskit.ir.gates import U3Gate
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 _logger = logging.getLogger(__name__)
 
@@ -143,8 +143,8 @@ def __init__(
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -154,7 +154,7 @@ def gen_initial_layer(
                 `self.initial_layer_gate`.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -170,11 +170,7 @@ def gen_initial_layer(
             init_circuit.append_gate(self.initial_layer_gate, [i])
         return init_circuit
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
@@ -189,7 +185,7 @@ def gen_successors(
             raise ValueError('Cannot expand a single-qudit circuit.')
 
         # Get the coupling graph
-        coupling_graph = BasePass.get_connectivity(circuit, data)
+        coupling_graph = data.connectivity
 
         # Generate successors
         successors = []
diff --git a/bqskit/passes/search/generators/single.py b/bqskit/passes/search/generators/single.py
index 9ed1336ae..0b0afcfdd 100644
--- a/bqskit/passes/search/generators/single.py
+++ b/bqskit/passes/search/generators/single.py
@@ -2,12 +2,13 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 _logger = logging.getLogger(__name__)
 
@@ -64,8 +65,8 @@ def __init__(self, gates: list[Gate], allow_repeats: bool = False) -> None:
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -77,7 +78,7 @@ def gen_initial_layer(
             ValueError: If `target` is not single-qudit.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -91,11 +92,7 @@ def gen_initial_layer(
         init_circuit = Circuit(target.num_qudits, target.radixes)
         return init_circuit
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
diff --git a/bqskit/passes/search/generators/stair.py b/bqskit/passes/search/generators/stair.py
index e7aa66fdd..ec0f18aee 100644
--- a/bqskit/passes/search/generators/stair.py
+++ b/bqskit/passes/search/generators/stair.py
@@ -2,12 +2,13 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -31,8 +32,8 @@ def __init__(self, gate: Gate) -> None:
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -42,7 +43,7 @@ def gen_initial_layer(
                 `self.seed`.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -56,11 +57,7 @@ def gen_initial_layer(
 
         return init_circuit
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
diff --git a/bqskit/passes/search/generators/wide.py b/bqskit/passes/search/generators/wide.py
index 16307c551..ceaa92da0 100644
--- a/bqskit/passes/search/generators/wide.py
+++ b/bqskit/passes/search/generators/wide.py
@@ -2,16 +2,17 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import IToffoliGate
 from bqskit.ir.gates import U3Gate
 from bqskit.passes.search.generator import LayerGenerator
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 _logger = logging.getLogger(__name__)
 
@@ -82,8 +83,8 @@ def __init__(
 
     def gen_initial_layer(
         self,
-        target: UnitaryMatrix | StateVector,
-        data: dict[str, Any],
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
     ) -> Circuit:
         """
         Generate the initial layer, see LayerGenerator for more.
@@ -93,7 +94,7 @@ def gen_initial_layer(
                 `self.initial_layer_gate`.
         """
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
@@ -109,11 +110,7 @@ def gen_initial_layer(
             init_circuit.append_gate(self.single_qudit_gate, [i])
         return init_circuit
 
-    def gen_successors(
-        self,
-        circuit: Circuit,
-        data: dict[str, Any],
-    ) -> list[Circuit]:
+    def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
         """
         Generate the successors of a circuit node.
 
diff --git a/bqskit/passes/search/heuristic.py b/bqskit/passes/search/heuristic.py
index 22de9a786..cad59b3c7 100644
--- a/bqskit/passes/search/heuristic.py
+++ b/bqskit/passes/search/heuristic.py
@@ -5,6 +5,7 @@
 
 from bqskit.ir.circuit import Circuit
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -19,14 +20,14 @@ class HeuristicFunction(abc.ABC):
     def get_value(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Return the heuristic's value given `circuit` and `target`."""
 
     def __call__(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Return the heuristic's value given `circuit` and `target`."""
 
@@ -35,7 +36,7 @@ def __call__(
                 'Expected circuit, got %s.' % type(circuit),
             )
 
-        if not isinstance(target, (UnitaryMatrix, StateVector)):
+        if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
                 'Expected unitary or state, got %s.' % type(target),
             )
diff --git a/bqskit/passes/search/heuristics/astar.py b/bqskit/passes/search/heuristics/astar.py
index 4ac58a3f4..9ad4e9a89 100644
--- a/bqskit/passes/search/heuristics/astar.py
+++ b/bqskit/passes/search/heuristics/astar.py
@@ -6,6 +6,7 @@
 from bqskit.ir.opt.cost import HilbertSchmidtCostGenerator
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_real_number
 
@@ -65,7 +66,7 @@ def __init__(
     def get_value(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Return the heuristic's value, see HeuristicFunction for more info."""
         cost = 0.0
diff --git a/bqskit/passes/search/heuristics/dijkstra.py b/bqskit/passes/search/heuristics/dijkstra.py
index 2608e9f80..d593267f0 100644
--- a/bqskit/passes/search/heuristics/dijkstra.py
+++ b/bqskit/passes/search/heuristics/dijkstra.py
@@ -4,6 +4,7 @@
 from bqskit.ir.circuit import Circuit
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -20,7 +21,7 @@ class DijkstraHeuristic(HeuristicFunction):
     def get_value(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Return the heuristic's value, see HeuristicFunction for more info."""
         cost = 0.0
diff --git a/bqskit/passes/search/heuristics/greedy.py b/bqskit/passes/search/heuristics/greedy.py
index 302e300ac..59d207aba 100644
--- a/bqskit/passes/search/heuristics/greedy.py
+++ b/bqskit/passes/search/heuristics/greedy.py
@@ -6,6 +6,7 @@
 from bqskit.ir.opt.cost import HilbertSchmidtCostGenerator
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
@@ -40,7 +41,7 @@ def __init__(
     def get_value(
         self,
         circuit: Circuit,
-        target: UnitaryMatrix | StateVector,
+        target: UnitaryMatrix | StateVector | StateSystem,
     ) -> float:
         """Return the heuristic's value, see HeuristicFunction for more info."""
         return self.cost_gen.calc_cost(circuit, target)
diff --git a/bqskit/passes/synthesis/__init__.py b/bqskit/passes/synthesis/__init__.py
index 0cc3ebaa3..e397671b6 100644
--- a/bqskit/passes/synthesis/__init__.py
+++ b/bqskit/passes/synthesis/__init__.py
@@ -6,6 +6,7 @@
 from bqskit.passes.synthesis.qpredict import QPredictDecompositionPass
 from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.passes.synthesis.target import SetTargetPass
 
 __all__ = [
     'LEAPSynthesisPass',
@@ -13,4 +14,5 @@
     'QPredictDecompositionPass',
     'QSearchSynthesisPass',
     'SynthesisPass',
+    'SetTargetPass',
 ]
diff --git a/bqskit/passes/synthesis/leap.py b/bqskit/passes/synthesis/leap.py
index 42e3911b6..c2d4a4d6f 100644
--- a/bqskit/passes/synthesis/leap.py
+++ b/bqskit/passes/synthesis/leap.py
@@ -7,6 +7,7 @@
 import numpy as np
 from scipy.stats import linregress
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
@@ -16,7 +17,10 @@
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.passes.search.heuristics import AStarHeuristic
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary import UnitaryMatrix
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_real_number
 
@@ -153,20 +157,25 @@ def __init__(
         self.store_partial_solutions = store_partial_solutions
         self.partials_per_depth = partials_per_depth
 
-    def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
+    async def synthesize(
+        self,
+        utry: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
+    ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
         frontier = Frontier(utry, self.heuristic_function)
-        data['window_markers'] = []
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
 
         # Seed the search with an initial layer
         initial_layer = self.layer_gen.gen_initial_layer(utry, data)
-        initial_layer = self.execute(
-            data,
+        initial_layer = await get_runtime().submit(
             Circuit.instantiate,
-            [initial_layer],
+            initial_layer,
             target=utry,
-            **self.instantiate_options,
-        )[0]
+            **instantiate_options,
+        )
         frontier.add(initial_layer, 0)
 
         # Track best circuit, initially the initial layer
@@ -195,12 +204,11 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
             successors = self.layer_gen.gen_successors(top_circuit, data)
 
             # Instantiate successors
-            circuits = self.execute(
-                data,
+            circuits = await get_runtime().map(
                 Circuit.instantiate,
                 successors,
                 target=utry,
-                **self.instantiate_options,
+                **instantiate_options,
             )
 
             # Evaluate successors
@@ -232,7 +240,6 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
                         _logger.debug(f'Prefix formed at {layer + 1} layers.')
                         last_prefix_layer = layer + 1
                         frontier.clear()
-                        data['window_markers'].append(circuit.num_cycles)
                         if self.max_layer is None or layer + 1 < self.max_layer:
                             frontier.add(circuit, layer + 1)
 
diff --git a/bqskit/passes/synthesis/qfast.py b/bqskit/passes/synthesis/qfast.py
index f0e47abc6..fc6ebf890 100644
--- a/bqskit/passes/synthesis/qfast.py
+++ b/bqskit/passes/synthesis/qfast.py
@@ -6,6 +6,7 @@
 from typing import Any
 from typing import Sequence
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import PauliGate
@@ -15,6 +16,8 @@
 from bqskit.ir.opt.cost import CostFunctionGenerator
 from bqskit.ir.opt.cost import HilbertSchmidtResidualsGenerator
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_real_number
@@ -123,13 +126,19 @@ def __init__(
         }
         self.instantiate_options.update(instantiate_options)
 
-    def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
+    async def synthesize(
+        self,
+        utry: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
+    ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
 
         # Skip any unitaries too small for the configured gate.
         if self.gate.num_qudits > utry.num_qudits:
-            _logger.warning('Skipping unitary synthesis since gate is larger.')
-            return Circuit.from_unitary(utry)
+            raise RuntimeError('Gate is too small for circuit')
 
         # Create empty circuit with same size and radixes as `utry`.
         circuit = Circuit(utry.num_qudits, utry.radixes)
@@ -149,19 +158,12 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
         while True:
 
             # Instantiate circuit
-            circuit = self.execute(
-                data,
-                Circuit.instantiate,
-                [circuit],
-                target=utry,
-                **self.instantiate_options,
-            )[0]
-
+            circuit.instantiate(utry, **instantiate_options)
             dist = self.cost.calc_cost(circuit, utry)
             _logger.info(f'Instantiated depth {depth} at {dist} cost.')
 
             if dist < self.success_threshold:
-                self.finalize(circuit, utry, data)
+                self.finalize(circuit, utry, instantiate_options)
                 _logger.info('Successful synthesis.')
                 return circuit
 
@@ -198,8 +200,8 @@ def get_location_of_head(self, circuit: Circuit) -> CircuitLocation:
     def finalize(
         self,
         circuit: Circuit,
-        utry: UnitaryMatrix,
-        data: dict[str, Any],
+        utry: UnitaryMatrix | StateVector | StateSystem,
+        instantiate_options: dict[str, Any],
     ) -> None:
         """Finalize the circuit by replacing the head with self.gate."""
         # Replace Head with self.gate
@@ -211,15 +213,7 @@ def finalize(
         # Reinstantiate
         dist = self.cost.calc_cost(circuit, utry)
         while dist > self.success_threshold:
-            circuit.become(
-                self.execute(
-                    data,
-                    Circuit.instantiate,
-                    [circuit],
-                    target=utry,
-                    **(self.instantiate_options),
-                )[0],
-            )
+            circuit.instantiate(utry, **instantiate_options)
             dist = self.cost.calc_cost(circuit, utry)
 
         _logger.info(f'Final circuit found with cost: {dist}.')
diff --git a/bqskit/passes/synthesis/qpredict.py b/bqskit/passes/synthesis/qpredict.py
index dd2d65403..4eaf035d7 100644
--- a/bqskit/passes/synthesis/qpredict.py
+++ b/bqskit/passes/synthesis/qpredict.py
@@ -7,12 +7,15 @@
 
 import numpy as np
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import VariableUnitaryGate
 from bqskit.ir.location import CircuitLocation
 from bqskit.ir.opt.cost import CostFunctionGenerator
 from bqskit.ir.opt.cost import HilbertSchmidtResidualsGenerator
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.math import pauli_expansion
 from bqskit.utils.math import unitary_log_no_i
@@ -180,8 +183,18 @@ def __init__(
         }
         self.instantiate_options.update(instantiate_options)
 
-    def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
+    async def synthesize(
+        self,
+        utry: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
+    ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
+        if isinstance(utry, (StateVector, StateSystem)):
+            raise RuntimeError('Unable to synthesize a state with qpredict.')
+
+        instantiate_options = self.instantiate_options.copy()
+        if data.seed is not None:
+            instantiate_options['seed'] = data.seed
 
         # 0. Skip any unitaries too small for the configured block.
         if self.block_size_start >= utry.num_qudits:
@@ -224,7 +237,7 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
                 circuit.append_gate(VariableUnitaryGate(len(loc)), loc)
                 circuit.instantiate(
                     utry,
-                    **self.instantiate_options,  # type: ignore
+                    **instantiate_options,  # type: ignore
                 )
                 cost = self.cost.calc_cost(circuit, utry)
                 _logger.info(f'Instantiated; layers: {layer}, cost: {cost:e}.')
diff --git a/bqskit/passes/synthesis/qsearch.py b/bqskit/passes/synthesis/qsearch.py
index f10ccc712..5eac51c69 100644
--- a/bqskit/passes/synthesis/qsearch.py
+++ b/bqskit/passes/synthesis/qsearch.py
@@ -4,6 +4,7 @@
 import logging
 from typing import Any
 
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
@@ -13,7 +14,10 @@
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.passes.search.heuristics import AStarHeuristic
 from bqskit.passes.synthesis.synthesis import SynthesisPass
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary import UnitaryMatrix
+from bqskit.runtime import get_runtime
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_real_number
 
@@ -132,19 +136,26 @@ def __init__(
         self.store_partial_solutions = store_partial_solutions
         self.partials_per_depth = partials_per_depth
 
-    def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
+    async def synthesize(
+        self,
+        utry: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
+    ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
+
         frontier = Frontier(utry, self.heuristic_function)
 
         # Seed the search with an initial layer
         initial_layer = self.layer_gen.gen_initial_layer(utry, data)
-        initial_layer = self.execute(
-            data,
+        initial_layer = await get_runtime().submit(
             Circuit.instantiate,
-            [initial_layer],
+            initial_layer,
             target=utry,
-            **self.instantiate_options,
-        )[0]
+            **instantiate_options,
+        )
         frontier.add(initial_layer, 0)
 
         # Track best circuit, initially the initial layer
@@ -170,12 +181,11 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
             successors = self.layer_gen.gen_successors(top_circuit, data)
 
             # Instantiate successors
-            circuits = self.execute(
-                data,
+            circuits = await get_runtime().map(
                 Circuit.instantiate,
                 successors,
                 target=utry,
-                **self.instantiate_options,
+                **instantiate_options,
             )
 
             # Evaluate successors
diff --git a/bqskit/passes/synthesis/synthesis.py b/bqskit/passes/synthesis/synthesis.py
index 03c370f63..8f52a0734 100644
--- a/bqskit/passes/synthesis/synthesis.py
+++ b/bqskit/passes/synthesis/synthesis.py
@@ -5,9 +5,11 @@
 from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.state.system import StateSystem
 
 if TYPE_CHECKING:
-    from typing import Any
     from bqskit.ir.circuit import Circuit
     from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
@@ -25,7 +27,11 @@ class SynthesisPass(BasePass):
     """
 
     @abstractmethod
-    def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
+    async def synthesize(
+        self,
+        target: UnitaryMatrix | StateVector | StateSystem,
+        data: PassData,
+    ) -> Circuit:
         """
         Synthesis abstract method to synthesize a UnitaryMatrix into a Circuit.
 
@@ -41,10 +47,6 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
             This function should be self-contained and have no side effects.
         """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        if len(data) == 0:
-            data = dict()
-
-        target_utry = self.get_target(circuit, data)
-        circuit.become(self.synthesize(target_utry, data))
+        circuit.become(await self.synthesize(data.target, data))
diff --git a/bqskit/passes/synthesis/target.py b/bqskit/passes/synthesis/target.py
new file mode 100644
index 000000000..97c2e0a90
--- /dev/null
+++ b/bqskit/passes/synthesis/target.py
@@ -0,0 +1,39 @@
+"""This module implements the SetTargetPass class."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.qis.state import StateSystem
+from bqskit.qis.state import StateVector
+from bqskit.qis.unitary import UnitaryMatrix
+
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+
+class SetTargetPass(BasePass):
+    """Sets a synthesis target for future passes."""
+
+    def __init__(
+        self,
+        target: UnitaryMatrix | StateVector | StateSystem,
+    ) -> None:
+        """
+        Construct a SetTargetPass.
+
+        Args:
+            target (UnitaryMatrix | StateVector | StateSystem): The target
+                to synthesize in future passes.
+        """
+        if not isinstance(target, (StateVector, UnitaryMatrix, StateSystem)):
+            bad_type = type(target)
+            m = f'Expected valid unitary or state for target, got {bad_type}.'
+            raise TypeError(m)
+
+        self.target = target
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        data.target = self.target
diff --git a/bqskit/passes/util/compress.py b/bqskit/passes/util/compress.py
index ff42e0fe6..df0a79cf6 100644
--- a/bqskit/passes/util/compress.py
+++ b/bqskit/passes/util/compress.py
@@ -1,9 +1,8 @@
 """This module implements the CompressPass class."""
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -15,6 +14,6 @@ class CompressPass(BasePass):
     passes slightly more efficient without actually changing the circuit.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         circuit.compress()
diff --git a/bqskit/passes/util/conversion.py b/bqskit/passes/util/conversion.py
index 60b75e2b2..07cbe758a 100644
--- a/bqskit/passes/util/conversion.py
+++ b/bqskit/passes/util/conversion.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import ConstantUnitaryGate
@@ -68,7 +68,7 @@ def __init__(
         else:
             raise ValueError('Unexpected input for conversion target.')
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
         # Variable -> Constant
diff --git a/bqskit/passes/util/converttou3.py b/bqskit/passes/util/converttou3.py
index b2ec0a905..f86b38d22 100644
--- a/bqskit/passes/util/converttou3.py
+++ b/bqskit/passes/util/converttou3.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.parameterized.pauli import PauliGate
 from bqskit.ir.gates.parameterized.u3 import U3Gate
@@ -17,7 +17,7 @@
 class ToU3Pass(BasePass):
     """Converts single-qubit general unitary gates to U3 Gates."""
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug('Converting single-qubit general gates to U3Gates.')
         for cycle, op in circuit.operations_with_cycles():
diff --git a/bqskit/passes/util/extend.py b/bqskit/passes/util/extend.py
index 7ba189621..682a30978 100644
--- a/bqskit/passes/util/extend.py
+++ b/bqskit/passes/util/extend.py
@@ -2,10 +2,10 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.operation import Operation
@@ -31,7 +31,7 @@ def __init__(self, minimum_size: int) -> None:
 
         self.minimum_size = minimum_size
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         if circuit.num_qudits < self.minimum_size:
             raise RuntimeError('Cannot extend block larger than circuit.')
diff --git a/bqskit/passes/util/fill.py b/bqskit/passes/util/fill.py
index 5a384c1c8..2761f961d 100644
--- a/bqskit/passes/util/fill.py
+++ b/bqskit/passes/util/fill.py
@@ -2,11 +2,13 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.gates.parameterized.u8 import U8Gate
+from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 
 
 _logger = logging.getLogger(__name__)
@@ -25,21 +27,27 @@ def __init__(self, success_threshold: float = 1e-10):
         """
         self.success_threshold = success_threshold
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug('Completing circuit with single-qudit gates.')
-        target = self.get_target(circuit, data)
+        target = data.target
 
         complete_circuit = Circuit(circuit.num_qudits, circuit.radixes)
 
-        if target.num_qudits == 1:
-            params = U3Gate.calc_params(target)
+        if target.num_qudits == 1 and circuit.radixes[0] == 2:
+            params = U3Gate.calc_params(circuit.get_unitary())
             complete_circuit.append_gate(U3Gate(), 0, params)
             circuit.become(complete_circuit)
             return
 
         for q in range(circuit.num_qudits):
-            complete_circuit.append_gate(U3Gate(), q)
+            radix = circuit.radixes[q]
+            if radix == 2:
+                complete_circuit.append_gate(U3Gate(), q)
+            elif radix == 3:
+                complete_circuit.append_gate(U8Gate(), q)
+            else:
+                complete_circuit.append_gate(VariableUnitaryGate(1), q)
 
         for op in circuit:
             if op.num_qudits == 1:
@@ -47,12 +55,21 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
             complete_circuit.append(op)
             for q in op.location:
-                complete_circuit.append_gate(U3Gate(), q)
+                radix = circuit.radixes[q]
+                if radix == 2:
+                    complete_circuit.append_gate(U3Gate(), q)
+                elif radix == 3:
+                    complete_circuit.append_gate(U8Gate(), q)
+                else:
+                    complete_circuit.append_gate(VariableUnitaryGate(1), q)
 
         dist = 1.0
         for i in range(10):
             complete_circuit.instantiate(target)
-            dist = complete_circuit.get_unitary().get_distance_from(target, 1)
+            dist = complete_circuit.get_unitary().get_distance_from(target, 1)  # type: ignore  # noqa
+            # TODO: State update
 
         if dist <= self.success_threshold:
             circuit.become(complete_circuit)
+        else:
+            _logger.warning('Unable to instantiate completed circuit.')
diff --git a/bqskit/passes/util/log.py b/bqskit/passes/util/log.py
index 0b8646b57..dc93e2fc5 100644
--- a/bqskit/passes/util/log.py
+++ b/bqskit/passes/util/log.py
@@ -2,11 +2,11 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -28,7 +28,7 @@ def __init__(self, msg: str, level: int = logging.INFO) -> None:
         self.msg = msg
         self.level = level
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.log(self.level, self.msg)
 
@@ -47,7 +47,7 @@ def __init__(self, error_threshold: float | None = None) -> None:
         """
         self.threshold = error_threshold
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         if 'error' in data:
             error = data['error']
diff --git a/bqskit/passes/util/random.py b/bqskit/passes/util/random.py
index 6a0b0a36a..33747492d 100644
--- a/bqskit/passes/util/random.py
+++ b/bqskit/passes/util/random.py
@@ -2,14 +2,10 @@
 from __future__ import annotations
 
 import logging
-import random
-from typing import Any
-
-import numpy as np
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
-from bqskit.utils.random import seed_random_sources
 from bqskit.utils.typing import is_integer
 
 
@@ -23,8 +19,6 @@ class SetRandomSeedPass(BasePass):
     The SetRandomSeedPass sets the random seed.
     """
 
-    key = '_SetRandomSeedPass_random_seed'
-
     def __init__(self, seed: int = 0) -> None:
         """
         Construct a SetRandomSeedPass.
@@ -38,10 +32,7 @@ def __init__(self, seed: int = 0) -> None:
 
         self.seed = seed
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug(f'Setting the random seed to {self.seed}.')
-        data[self.key] = self.seed
-        np.random.seed(self.seed)
-        random.seed(self.seed)
-        seed_random_sources(self.seed)
+        data.seed = self.seed
diff --git a/bqskit/passes/util/record.py b/bqskit/passes/util/record.py
index d2574d052..f9650f9e1 100644
--- a/bqskit/passes/util/record.py
+++ b/bqskit/passes/util/record.py
@@ -5,6 +5,7 @@
 from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
@@ -19,7 +20,7 @@ class RecordStatsPass(BasePass):
 
     key = 'RecordStatsPass_stats_list'
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         stats: dict[str, Any] = {}
         stats['cycles'] = circuit.num_cycles
diff --git a/bqskit/passes/util/unfold.py b/bqskit/passes/util/unfold.py
index c5aa5528c..5ffad8606 100644
--- a/bqskit/passes/util/unfold.py
+++ b/bqskit/passes/util/unfold.py
@@ -2,9 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -18,7 +18,7 @@ class UnfoldPass(BasePass):
     The UnfoldPass unfolds all CircuitGate blocks into the circuit.
     """
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug('Unfolding the circuit.')
         circuit.unfold_all()
diff --git a/bqskit/passes/util/update.py b/bqskit/passes/util/update.py
index 02f23dcac..849437061 100644
--- a/bqskit/passes/util/update.py
+++ b/bqskit/passes/util/update.py
@@ -5,6 +5,7 @@
 from typing import Any
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
@@ -34,7 +35,7 @@ def __init__(self, key: str, val: Any) -> None:
         self.key = key
         self.val = val
 
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+    async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         _logger.debug(f'Injecting {self.key}:{self.val} into the data dict.')
         data[self.key] = self.val
diff --git a/bqskit/qis/__init__.py b/bqskit/qis/__init__.py
index d54b3c5a6..b20a61590 100644
--- a/bqskit/qis/__init__.py
+++ b/bqskit/qis/__init__.py
@@ -57,6 +57,8 @@
 from bqskit.qis.state import StateLike
 from bqskit.qis.state import StateVector
 from bqskit.qis.state import StateVectorMap
+from bqskit.qis.state.system import StateSystem
+from bqskit.qis.state.system import StateSystemLike
 from bqskit.qis.unitary import DifferentiableUnitary
 from bqskit.qis.unitary import LocallyOptimizableUnitary
 from bqskit.qis.unitary import RealVector
@@ -80,4 +82,6 @@
     'RealVector',
     'CouplingGraph',
     'CouplingGraphLike',
+    'StateSystem',
+    'StateSystemLike',
 ]
diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py
index 1df8c7233..06994ef34 100644
--- a/bqskit/qis/graph.py
+++ b/bqskit/qis/graph.py
@@ -39,7 +39,7 @@ def __init__(
         if isinstance(graph, CouplingGraph):
             self.num_qudits: int = graph.num_qudits
             self._edges: set[tuple[int, int]] = graph._edges
-            self._adj: list[list[int]] = graph._adj
+            self._adj: list[set[int]] = graph._adj
             return
 
         if not CouplingGraph.is_valid_coupling_graph(graph):
@@ -59,10 +59,10 @@ def __init__(
         else:
             self.num_qudits = num_qudits
 
-        self._adj = [[] for _ in range(self.num_qudits)]
+        self._adj = [set() for _ in range(self.num_qudits)]
         for q1, q2 in self._edges:
-            self._adj[q1].append(q2)
-            self._adj[q2].append(q1)
+            self._adj[q1].add(q2)
+            self._adj[q2].add(q1)
 
         self._mat = [
             [np.inf for _ in range(self.num_qudits)]
@@ -94,7 +94,7 @@ def is_fully_connected(self) -> bool:
 
     def get_neighbors_of(self, qudit: int) -> list[int]:
         """Return the qudits adjacent to `qudit`."""
-        return self._adj[qudit]
+        return list(self._adj[qudit])
 
     def __contains__(self, __o: object) -> bool:
         return self._edges.__contains__(__o)
@@ -190,9 +190,10 @@ def get_subgraph(
             renumbering = {q: i for i, q in enumerate(location)}
 
         subgraph = []
-        for q0, q1 in self._edges:
-            if q0 in location and q1 in location:
-                subgraph.append((renumbering[q0], renumbering[q1]))
+        location_set = {loc for loc in location}
+        for q_i in location:
+            for q_i_neighbor in location_set.intersection(self._adj[q_i]):
+                subgraph.append((renumbering[q_i], renumbering[q_i_neighbor]))
         return CouplingGraph(subgraph, len(location))
 
     def get_subgraphs_of_size(self, size: int) -> list[CircuitLocation]:
diff --git a/bqskit/qis/state/__init__.py b/bqskit/qis/state/__init__.py
index 1bdd77323..24785045c 100644
--- a/bqskit/qis/state/__init__.py
+++ b/bqskit/qis/state/__init__.py
@@ -4,9 +4,13 @@
 from bqskit.qis.state.state import StateLike
 from bqskit.qis.state.state import StateVector
 from bqskit.qis.state.statemap import StateVectorMap
+from bqskit.qis.state.system import StateSystem
+from bqskit.qis.state.system import StateSystemLike
 
 __all__ = [
     'StateVector',
     'StateLike',
     'StateVectorMap',
+    'StateSystem',
+    'StateSystemLike',
 ]
diff --git a/bqskit/qis/state/state.py b/bqskit/qis/state/state.py
index 221e26a79..82226f592 100644
--- a/bqskit/qis/state/state.py
+++ b/bqskit/qis/state/state.py
@@ -188,6 +188,15 @@ def is_pure_state(V: Any, tol: float = 1e-8) -> TypeGuard[StateLike]:
 
         return True
 
+    @staticmethod
+    def zero(num_qudits: int, radixes: Sequence[int] = []) -> StateVector:
+        """Prepares the zero state."""
+        if len(radixes) == 0:
+            radixes = [2] * num_qudits
+        state = np.zeros(np.prod(radixes), dtype=np.complex128)
+        state[0] = 1.0
+        return StateVector(state)
+
     @staticmethod
     def random(num_qudits: int, radixes: Sequence[int] = []) -> StateVector:
         """
@@ -230,6 +239,10 @@ def random(num_qudits: int, radixes: Sequence[int] = []) -> StateVector:
         U = unitary_group.rvs(int(np.prod(radixes)))
         return StateVector(U[:, 0], radixes)
 
+    def __hash__(self) -> int:
+        """Hash the state vector."""
+        return hash(tuple(self.numpy))
+
     def __eq__(self, other: object) -> bool:
         """Check if `self` is approximately equal to `other`."""
         if isinstance(other, StateVector):
@@ -240,6 +253,24 @@ def __eq__(self, other: object) -> bool:
 
         return NotImplemented
 
+    def get_distance_from(self, other: StateLike) -> float:
+        """
+        Return the distance between `self` and `other`.
+
+        The distance is given as the infidelity between the two states.
+
+        Args:
+            other (StateLike): The state to measure distance from.
+
+        Returns:
+            float: A value between 1 and 0, where 0 means the two states
+            are equal up to global phase and 1 means the two states are
+            very unsimilar or far apart.
+        """
+        other = StateVector(other)
+        dist = 1 - np.abs(np.conj(self) @ other) ** 2
+        return dist if dist > 0.0 else 0.0
+
     def __array__(
         self,
         dtype: np.typing.DTypeLike = np.complex128,
diff --git a/bqskit/qis/state/system.py b/bqskit/qis/state/system.py
new file mode 100644
index 000000000..b64d7162c
--- /dev/null
+++ b/bqskit/qis/state/system.py
@@ -0,0 +1,121 @@
+"""This module implements the StateSystem class."""
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import Iterator
+from typing import Mapping
+from typing import TYPE_CHECKING
+from typing import Union
+
+import numpy as np
+
+from bqskit.qis.state.state import StateVector
+
+if TYPE_CHECKING:
+    import numpy.typing as npt
+    from typing import TypeGuard
+
+
+class StateSystem(Mapping[StateVector, StateVector]):
+    """A system of input and output states."""
+
+    def __init__(self, system: StateSystemLike) -> None:
+        """Construct a state system."""
+        if isinstance(system, StateSystem):
+            self._system: dict[StateVector, StateVector] = system._system
+            self._radixes: tuple[int, ...] = system._radixes
+            self._dim: int = system._dim
+            self._vec_count: int = system._vec_count
+            self.target: npt.NDArray[np.complex128] = system.target
+            return
+
+        self._system = {
+            StateVector(k): StateVector(v)
+            for k, v in system.items()
+        }
+        self._radixes = list(self._system.keys())[0].radixes
+        self._dim = list(self._system.keys())[0].dim
+        self._vec_count = len(self._system)
+
+        for k, v in system.items():
+            if k.radixes != self.radixes:
+                raise ValueError('States in system have mismatch in dimension.')
+            if v.radixes != self.radixes:
+                raise ValueError('States in system have mismatch in dimension.')
+
+        # Check overlap matrices
+        V = np.column_stack(list(self._system.keys()))
+        W = np.column_stack(list(self._system.values()))
+        Ov = V.conj().T @ V
+        Wv = W.conj().T @ W
+        if not np.allclose(Ov, Wv):
+            raise ValueError(
+                'State system is unsolvable:'
+                ' input and output overlap matrices do not match.',
+            )
+        self.target = W @ V.conj().T
+
+    @property
+    def num_qudits(self) -> int:
+        """The number of qudits in the state."""
+        return len(self.radixes)
+
+    @property
+    def dim(self) -> int:
+        """The vector dimension for this state."""
+        return self._dim
+
+    @property
+    def radixes(self) -> tuple[int, ...]:
+        """The number of orthogonal states for each qudit."""
+        return self._radixes
+
+    def __iter__(self) -> Iterator[StateVector]:
+        return self._system.__iter__()
+
+    def __len__(self) -> int:
+        return self._system.__len__()
+
+    def __getitem__(self, _key: StateVector) -> StateVector:
+        return self._system.__getitem__(_key)
+
+    def __contains__(self, _key: object) -> bool:
+        return self._system.__contains__(_key)
+
+    def is_qubit_only(self) -> bool:
+        """Return true if this unitary can only act on qubits."""
+        return all([radix == 2 for radix in self.radixes])
+
+    def is_qutrit_only(self) -> bool:
+        """Return true if this unitary can only act on qutrits."""
+        return all([radix == 3 for radix in self.radixes])
+
+    @staticmethod
+    def is_state_system(V: Any) -> TypeGuard[StateSystemLike]:
+        """
+        Check if V is a system of pure states.
+
+        Args:
+            V (Any): The vector to check.
+
+        Returns:
+            bool: True if V is a system of pure states.
+        """
+        if isinstance(V, StateSystem):
+            return True
+
+        if not isinstance(V, dict):
+            return False
+
+        for k, v in V.items():
+            if not StateVector.is_pure_state(k):
+                return False
+
+            if not StateVector.is_pure_state(v):
+                return False
+
+        return True
+
+
+StateSystemLike = Union[StateSystem, Dict[Any, Any]]
diff --git a/bqskit/runtime/__init__.py b/bqskit/runtime/__init__.py
new file mode 100644
index 000000000..df16959a3
--- /dev/null
+++ b/bqskit/runtime/__init__.py
@@ -0,0 +1,189 @@
+"""
+=======================================================
+A Runtime for the Compile-time (:mod:`bqskit.runtime`)
+=======================================================
+
+.. currentmodule:: bqskit.runtime
+
+This package provides an execution environment for compiler passes,
+enabling them to parallelize and distribute their workload efficiently.
+
+Launching a Server
+##################
+
+To parallelize BQSKit compilations, you can launch a BQSKit Runtime
+server in either attached or detached mode, and submit a
+:class:`~bqskit.compiler.task.CompilationTask` through a connected
+:class:`~bqskit.compiler.compiler.Compiler`.
+
+By default, a compiler will automatically start and connect to an attached
+runtime server during initialization. In this mode, the compiler
+starts up and shuts down the server automatically and transparently to
+the end user. See :class:`~bqskit.runtime.attached.AttachedServer` for
+more info.
+
+A server running in attached mode is limited to a single machine. If you
+would like to parallelize BQSKit across multiple nodes over a network,
+you can launch a server independently in detached mode. Here, you first
+start :class:`~bqskit.runtime.manager.Manager` processes on all machines.
+Then link them all together by running a
+:class:`~bqskit.runtime.detached.DetachedServer` process, which Compilers'
+can connect to by passing the appropriate address in their constructor.
+
+Upon installing BQSKit, two shell scripts are added to your environment
+that can spawn managers and detached servers::
+
+    bqskit-manager
+
+and::
+
+    bqskit-server <address_of_bqskit_managers>
+
+Typically, you start managers first on all nodes in the desired cluster.
+Then you can start the server with a comma-separated list of all managers
+ip address and optionally ports. Once a server is started and has connected
+to the requested managers, no more managers can be added. You can see
+the `-h` option of each command or the
+:func:`~bqskit.runtime.detached.start_server` and
+:func:`~bqskit.runtime.manager.start_manager` entry points for
+more information.
+
+For more information on how to manage jobs submitted to a runtime server,
+see the :obj:`~bqskit.compiler` documentation.
+
+.. autosummary::
+    :toctree: autogen
+    :recursive:
+
+    ~bqskit.runtime.attached.AttachedServer
+    ~bqskit.runtime.detached.DetachedServer
+    ~bqskit.runtime.manager.Manager
+    ~bqskit.runtime.detached.start_server
+    ~bqskit.runtime.manager.start_manager
+
+Parallelize Pass Computation
+############################
+
+When developing a BQSKit compiler pass, you can hook into the active
+runtime server through the :func:`get_runtime` function. This returns a
+:class:`RuntimeHandle`, which you can use to submit, map, wait on, and
+cancel tasks in the execution environment.
+
+For more information on how to design a custom pass, see this (TODO, sorry,
+you can look at the source code of existing
+`passes <https://github.com/BQSKit/bqskit/blob/master/bqskit/passes>`_
+for a good example for the time being).
+
+.. autosummary::
+    :toctree: autogen
+    :recursive:
+
+    ~bqskit.runtime.get_runtime
+    ~bqskit.runtime.RuntimeHandle
+    ~bqskit.runtime.future.RuntimeFuture
+
+Standard Logging Interface
+##########################
+
+This system uses the standard python logging module to provide a familiar
+logging interface. On client processes -- the ones where you create the
+Compiler object -- you can configure python loggers like normal before
+submitting tasks and have the entire system honor that configuration.
+"""
+from __future__ import annotations
+
+import faulthandler
+import os
+from typing import Any
+from typing import Callable
+from typing import Protocol
+from typing import TYPE_CHECKING
+
+# Enable low-level fault handling: system crashes print a minimal trace.
+faulthandler.enable()
+
+
+# Disable multi-threading in BLAS libraries.
+os.environ['OMP_NUM_THREADS'] = '1'
+os.environ['OPENBLAS_NUM_THREADS'] = '1'
+os.environ['MKL_NUM_THREADS'] = '1'
+os.environ['NUMEXPR_NUM_THREADS'] = '1'
+os.environ['VECLIB_MAXIMUM_THREADS'] = '1'
+os.environ['RUST_BACKTRACE'] = '1'
+
+
+if TYPE_CHECKING:
+    from bqskit.runtime.future import RuntimeFuture
+
+
+class RuntimeHandle(Protocol):
+    """
+    A structural type capturing BQSKit Runtime capabilities.
+
+    A compiler pass designer can effortlessly parallelize certain aspects
+    of compilation using the :func:`submit` and :func:`map` methods.
+    These both will return :class:`~bqskit.runtime.future.RuntimeFuture`
+    objects, which can be awaited on using the standard python `await`
+    keyword. This can also be used to cancel tasks and wait on specific
+    events.
+
+    This should never be constructed directly and only accessed through
+    the :func:`get_runtime` function, since this is a structural type
+    definition and does not contain any implementation.
+    """
+
+    def submit(
+        self,
+        fn: Callable[..., Any],
+        *args: Any,
+        **kwargs: Any,
+    ) -> RuntimeFuture:
+        """Submit a `fn` to the runtime."""
+        ...
+
+    def map(
+        self,
+        fn: Callable[..., Any],
+        *args: Any,
+        **kwargs: Any,
+    ) -> RuntimeFuture:
+        """Map `fn` over the input arguments distributed across the runtime."""
+        ...
+
+    def cancel(self, future: RuntimeFuture) -> None:
+        """
+        Cancel all tasks associated with `future`.
+
+        This is asynchronous and non-pre-emptive. The cancel will return
+        immediately, the future it represents will become dead, and at some
+        point in the future, all tasks and data associated with the cancelled
+        one will be removed from the system.
+        """
+        ...
+
+    async def next(self, future: RuntimeFuture) -> list[tuple[int, Any]]:
+        """
+        Wait for and return the next batch of results from a map task.
+
+        Returns:
+            (list[tuple[int, Any]]): A list of the results that arrived
+                while the task was waiting. Each result is paired with
+                the index of its arguments in the original map call.
+        """
+        ...
+
+
+def get_runtime() -> RuntimeHandle:
+    """
+    Return a handle on the active runtime.
+
+    See :class:`RuntimeHandle` for more info.
+    """
+    from bqskit.runtime.worker import get_worker
+    return get_worker()
+
+
+default_server_port = 7472
+default_manager_port = 7473
+default_worker_port = 7474
+__all__ = ['get_runtime']
diff --git a/bqskit/runtime/address.py b/bqskit/runtime/address.py
new file mode 100644
index 000000000..8535588f3
--- /dev/null
+++ b/bqskit/runtime/address.py
@@ -0,0 +1,18 @@
+"""This module implements the RuntimeAddress NamedTuple."""
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class RuntimeAddress(NamedTuple):
+    """
+    An address to a mailbox on a server or worker.
+
+    These are also used as unique identification for a task, since in this
+    system, a mailbox slot has a one-to-one relationship with a task. A task
+    will ship its result to its return address. That slot will only accept a
+    package from that task.
+    """
+    worker_id: int
+    mailbox_index: int
+    mailbox_slot: int
diff --git a/bqskit/runtime/attached.py b/bqskit/runtime/attached.py
new file mode 100644
index 000000000..4b64d77f1
--- /dev/null
+++ b/bqskit/runtime/attached.py
@@ -0,0 +1,95 @@
+"""This module implements the AttachedServer runtime."""
+from __future__ import annotations
+
+import logging
+import selectors
+import uuid
+from multiprocessing.connection import Connection
+from typing import Any
+
+from bqskit.runtime import default_server_port
+from bqskit.runtime import default_worker_port
+from bqskit.runtime.base import ServerBase
+from bqskit.runtime.detached import DetachedServer
+from bqskit.runtime.detached import ServerMailbox
+from bqskit.runtime.direction import MessageDirection
+
+
+class AttachedServer(DetachedServer):
+    """
+    BQSKit Runtime Server in attached mode.
+
+    In attached mode, the runtime is started by the client. The client owns the
+    server and there is only one client on the server. Additionally, the client
+    is responsible for shutting down the server. There are no managers in the
+    attached architecture, only workers directly managed by a single server.
+    This architecture is designed for single-machine shared-memory settings.
+    BQSKit will, by default, create, manage, and shutdown an AttachedServer when
+    a :class:`~bqskit.compiler.compiler.Compiler` object is created.
+    """
+
+    def __init__(
+        self,
+        num_workers: int = -1,
+        port: int = default_server_port,
+        worker_port: int = default_worker_port,
+    ) -> None:
+        """
+        Create a server with `num_workers` workers.
+
+        Args:
+            num_workers (int): The number of workers to spawn. If -1,
+                then spawn as many workers as CPUs on the system.
+                (Default: -1).
+
+            port (int): The port this server will listen for clients on.
+                Default can be found in the
+                :obj:`~bqskit.runtime.default_server_port` global variable.
+
+            worker_port (int): The port this server will listen for workers
+                on. Default can be found in the
+                :obj:`~bqskit.runtime.default_worker_port` global variable.
+        """
+        ServerBase.__init__(self)
+
+        # See DetachedServer for more info on the following fields:
+        self.clients: dict[Connection, set[uuid.UUID]] = {}
+        self.tasks: dict[uuid.UUID, tuple[int, Connection]] = {}
+        self.mailbox_to_task_dict: dict[int, uuid.UUID] = {}
+        self.mailboxes: dict[int, ServerMailbox] = {}
+        self.mailbox_counter = 0
+
+        # Start workers
+        self.spawn_workers(num_workers)
+
+        # Connect to client
+        client_conn = self.listen_once(port)
+        self.clients[client_conn] = set()
+        self.sel.register(
+            client_conn,
+            selectors.EVENT_READ,
+            MessageDirection.CLIENT,
+        )
+        self.logger.info('Connected to client.')
+
+    def handle_disconnect(self, conn: Connection) -> None:
+        """A client disconnect in attached mode is equal to a shutdown."""
+        self.handle_shutdown()
+
+
+def start_attached_server(
+    num_workers: int,
+    log_level: int,
+    **kwargs: Any,
+) -> None:
+    """Start a runtime server in attached mode."""
+    # Initialize runtime logging
+    _logger = logging.getLogger('bqskit-runtime')
+    _logger.setLevel(log_level)
+    _logger.addHandler(logging.StreamHandler())
+
+    # Initialize the server
+    server = AttachedServer(num_workers, **kwargs)
+
+    # Run the server
+    server.run()
diff --git a/bqskit/runtime/base.py b/bqskit/runtime/base.py
new file mode 100644
index 000000000..d154ca841
--- /dev/null
+++ b/bqskit/runtime/base.py
@@ -0,0 +1,561 @@
+"""This module implements the NodeBase abstract class."""
+from __future__ import annotations
+
+import abc
+import functools
+import logging
+import os
+import random
+import selectors
+import signal
+import socket
+import sys
+import time
+import traceback
+from multiprocessing import Process
+from multiprocessing.connection import Client
+from multiprocessing.connection import Connection
+from multiprocessing.connection import Listener
+from queue import Queue
+from threading import Thread
+from types import FrameType
+from typing import Any
+from typing import cast
+from typing import Sequence
+
+from bqskit.runtime import default_manager_port
+from bqskit.runtime import default_worker_port
+from bqskit.runtime.address import RuntimeAddress
+from bqskit.runtime.direction import MessageDirection
+from bqskit.runtime.message import RuntimeMessage
+from bqskit.runtime.result import RuntimeResult
+from bqskit.runtime.task import RuntimeTask
+from bqskit.runtime.worker import start_worker
+
+
+class RuntimeEmployee:
+    """Data structure for a boss's view of an employee."""
+
+    def __init__(
+        self,
+        conn: Connection,
+        total_workers: int,
+        process: Process | None = None,
+        num_tasks: int = 0,
+    ) -> None:
+        """Construct an employee with all resources idle."""
+        self.conn: Connection = conn
+        self.total_workers = total_workers
+        self.process = process
+        self.num_tasks = num_tasks
+        self.num_idle_workers = total_workers
+
+    def shutdown(self) -> None:
+        """Shutdown the employee."""
+        try:
+            self.conn.send((RuntimeMessage.SHUTDOWN, None))
+        except Exception:
+            pass
+
+        if self.process is not None:
+            self.process.join()
+
+        self.process = None
+        self.conn.close()
+
+    @property
+    def has_idle_resources(self) -> bool:
+        return self.num_idle_workers > 0
+
+
+def send_outgoing(node: ServerBase) -> None:
+    """Outgoing thread forwards messages as they are created."""
+    while True:
+        outgoing = node.outgoing.get()
+
+        if not node.running:
+            # NodeBase's handle_shutdown will put a dummy value in the
+            # queue to wake the thread up so it can exit safely.
+            # Hence the node.running check now rather than in the
+            # while condition.
+            break
+
+        outgoing[0].send((outgoing[1], outgoing[2]))
+        node.logger.debug(f'Sent message {outgoing[1].name}.')
+        node.logger.log(1, f'{outgoing[2]}\n')
+
+
+def sigint_handler(signum: int, _: FrameType | None, node: ServerBase) -> None:
+    """Interrupt the node."""
+    if not node.running:
+        return
+
+    node.running = False
+    node.terminate_hotline.send(b'\0')
+    node.logger.info('Server interrupted.')
+
+
+class ServerBase:
+    """Base class for all non-worker process nodes in the BQSKit Runtime."""
+
+    def __init__(self) -> None:
+        """Initialize a runtime node component."""
+
+        self.lower_id_bound = 0
+        self.upper_id_bound = int(2 ** 30)
+        """
+        The node starts with an ID range from 0 -> 2^30. ID ranges are then
+        assigned to managers by evenly splitting this range.
+
+        Managers then recursively split their range when connecting the sub-
+        managers. Finally, workers are assigned specific ids from within this
+        range.
+        """
+
+        self.running = True
+        """True while the node is running."""
+
+        self.sel = selectors.DefaultSelector()
+        """Used to efficiently idle and wake when communication is ready."""
+
+        p, self.terminate_hotline = socket.socketpair()
+        self.sel.register(p, selectors.EVENT_READ, MessageDirection.SIGNAL)
+        """Terminate hotline is used to unblock select while running."""
+
+        self.logger = logging.getLogger('bqskit-runtime')
+        """Logger used to print operational log messages."""
+
+        self.employees: list[RuntimeEmployee] = []
+        """Tracks this node's employees, which are managers or workers."""
+
+        self.conn_to_employee_dict: dict[Connection, RuntimeEmployee] = {}
+        """Used to find the employee associated with a message."""
+
+        # Safely and immediately exit on interrupt signals
+        handle = functools.partial(sigint_handler, node=self)
+        signal.signal(signal.SIGINT, handle)
+
+        # Start outgoing thread
+        self.outgoing: Queue[tuple[Connection, RuntimeMessage, Any]] = Queue()
+        self.outgoing_thread = Thread(target=send_outgoing, args=(self,))
+        self.outgoing_thread.start()
+        self.logger.info('Started outgoing thread.')
+
+    def connect_to_managers(self, ipports: Sequence[tuple[str, int]]) -> None:
+        """Connect to all managers given by endpoints in `ipports`."""
+        d = len(ipports)
+        self.step_size = (self.upper_id_bound - self.lower_id_bound) // d
+
+        # Establish connections with all managers
+        manager_conns = []
+        for i, (ip, port) in enumerate(ipports):
+            lb = self.lower_id_bound + (i * self.step_size)
+            ub = min(
+                self.lower_id_bound + ((i + 1) * self.step_size),
+                self.upper_id_bound,
+            )
+            manager_conns.append(self.connect_to_manager(ip, port, lb, ub))
+            self.logger.info(f'Connected to manager {i} at {ip}:{port}.')
+            self.logger.debug(f'Gave bounds {lb=} and {ub=} to manager {i}.')
+
+        # Wait for started messages from all managers and register them
+        self.total_workers = 0
+        for i, conn in enumerate(manager_conns):
+            msg, num_workers = conn.recv()
+            assert msg == RuntimeMessage.STARTED
+            self.employees.append(RuntimeEmployee(conn, num_workers))
+            self.conn_to_employee_dict[conn] = self.employees[-1]
+            self.sel.register(
+                conn,
+                selectors.EVENT_READ,
+                MessageDirection.BELOW,
+            )
+            self.logger.info(f'Registered manager {i} with {num_workers = }.')
+            self.total_workers += num_workers
+        self.num_idle_workers = self.total_workers
+
+        self.logger.info(f'Node has {self.total_workers} total workers.')
+
+    def connect_to_manager(
+        self,
+        ip: str,
+        port: int,
+        lb: int,
+        ub: int,
+    ) -> Connection:
+        """
+        Connect to a manager at the endpoint given by `ip` and `port`.
+
+        Args:
+            ip (str): The IP address where the manager is expected to be
+                listening.
+
+            port (int): The port number on which the manager is expected
+                to be listening.
+
+            lb (int): The ID lower bound to send to the manager.
+
+            ub (int): The ID upper bound to send to the manager.
+        """
+        max_retries = 5
+        wait_time = .25
+
+        for _ in range(max_retries):
+            try:
+                conn = Client((ip, port))
+            except ConnectionRefusedError:
+                time.sleep(wait_time)
+                wait_time *= 2
+            else:
+                conn.send((RuntimeMessage.CONNECT, (lb, ub)))
+                return conn
+
+        raise RuntimeError(f'Manager connection refused at {ip}:{port}')
+
+    def spawn_workers(
+        self,
+        num_workers: int = -1,
+        port: int = default_worker_port,
+    ) -> None:
+        """
+        Spawn worker processes.
+
+        Args:
+            num_workers (int): The number of workers to spawn. If -1,
+                then spawn as many workers as CPUs on the system.
+                (Default: -1).
+
+            port (int): The port this server will listen for workers on.
+                Default can be found in the
+                :obj:`~bqskit.runtime.default_worker_port` global variable.
+        """
+        if num_workers == -1:
+            oscount = os.cpu_count()
+            num_workers = oscount if oscount else 1
+
+        if self.lower_id_bound + num_workers >= self.upper_id_bound:
+            raise RuntimeError('Insufficient id range for workers.')
+
+        # Create and start all worker processed
+        procs = {}
+        for i in range(num_workers):
+            w_id = self.lower_id_bound + i
+            procs[w_id] = Process(target=start_worker, args=(w_id, port))
+            procs[w_id].start()
+            self.logger.debug(f'Stated worker process {i}.')
+
+        # Listen for the worker connections
+        family = 'AF_INET' if sys.platform == 'win32' else None
+        listener = Listener(('localhost', port), family, backlog=num_workers)
+        conns = [listener.accept() for _ in range(num_workers)]
+        listener.close()
+
+        # Organize all workers into the employees data structure
+        temp_reorder = {}
+        for i, conn in enumerate(conns):
+            msg, w_id = conn.recv()
+            assert msg == RuntimeMessage.STARTED
+            employee = RuntimeEmployee(conn, 1, procs[w_id])
+            temp_reorder[w_id - self.lower_id_bound] = employee
+            self.conn_to_employee_dict[conn] = employee
+
+        # The employess list needs to be sorted according to the IDs
+        for i in range(num_workers):
+            self.employees.append(temp_reorder[i])
+
+        # Register employee communication
+        for i, employee in enumerate(self.employees):
+            self.sel.register(
+                employee.conn,
+                selectors.EVENT_READ,
+                MessageDirection.BELOW,
+            )
+            self.logger.info(f'Registered worker {i}.')
+
+        self.step_size = 1
+        self.total_workers = num_workers
+        self.num_idle_workers = num_workers
+        self.logger.info(f'Node has spawned {num_workers} workers.')
+
+    def listen_once(self, port: int) -> Connection:
+        """Listen on `port` for a connection and return on first one."""
+        family = 'AF_INET' if sys.platform == 'win32' else None
+        listener = Listener(('0.0.0.0', port), family)
+        conn = listener.accept()
+        listener.close()
+        return conn
+
+    def run(self) -> None:
+        """Main loop."""
+        self.logger.info(f'{self.__class__.__name__} running...')
+
+        try:
+            while self.running:
+                # Wait for messages
+                events = self.sel.select()  # Say that 5 times fast
+
+                for key, _ in events:
+                    # Unpack message, payload, and direction.
+                    conn = cast(Connection, key.fileobj)
+                    direction = cast(MessageDirection, key.data)
+
+                    # If interrupted by signal, shutdown and exit
+                    if direction == MessageDirection.SIGNAL:
+                        self.logger.debug('Received interrupt signal.')
+                        self.handle_shutdown()
+                        return
+
+                    # Unpack and Log message
+                    try:
+                        msg, payload = conn.recv()
+                    except (EOFError, ConnectionResetError):
+                        self.handle_disconnect(conn)
+                        continue
+                    log = f'Received message {msg.name} from {direction.name}.'
+                    self.logger.debug(log)
+                    self.logger.log(1, f'{payload}\n')
+
+                    # Handle message
+                    self.handle_message(msg, direction, conn, payload)
+
+        except Exception:
+            exc_info = sys.exc_info()
+            error_str = ''.join(traceback.format_exception(*exc_info))
+            self.logger.error(error_str)
+            self.handle_system_error(error_str)
+
+        finally:
+            self.handle_shutdown()
+
+    @abc.abstractmethod
+    def handle_message(
+        self,
+        msg: RuntimeMessage,
+        direction: MessageDirection,
+        conn: Connection,
+        payload: Any,
+    ) -> None:
+        """
+        Process the message coming from `direction`.
+
+        Args:
+            msg (RuntimeMessage): The message type to handle.
+
+            direction (MessageDirection): The direction the message came from.
+
+            conn (Connection): The connection object where this came from.
+
+            payload (Any): The message data.
+        """
+
+    @abc.abstractmethod
+    def handle_system_error(self, error_str: str) -> None:
+        """
+        Handle an error in runtime code as opposed to client code.
+
+        This is called when an error arises in runtime code not in a
+        RuntimeTask's coroutine code.
+        """
+
+    def handle_shutdown(self) -> None:
+        """Shutdown the node and release resources."""
+        # Stop running
+        self.logger.info('Shutting down node.')
+        self.running = False
+
+        # Instruct employees to shutdown
+        for employee in self.employees:
+            employee.shutdown()
+        self.employees.clear()
+        self.logger.debug('Shutdown employees.')
+
+        # Close selector
+        self.sel.close()
+        self.logger.debug('Cleared selector.')
+
+        # Close outgoing thread
+        if self.outgoing_thread.is_alive():
+            self.outgoing.put(b'\0')  # type: ignore
+            self.outgoing_thread.join()
+            self.logger.debug('Joined outgoing thread.')
+            assert not self.outgoing_thread.is_alive()
+
+    def handle_disconnect(self, conn: Connection) -> None:
+        """Remove `conn` from the server."""
+        self.sel.unregister(conn)
+        conn.close()
+
+        # If one of my employees crashed/shutdown/disconnected, I shutdown
+        if conn in self.conn_to_employee_dict:
+            self.handle_shutdown()
+
+    def __del__(self) -> None:
+        """Ensure resources are cleaned up."""
+        self.handle_shutdown()
+
+    def assign_tasks(
+        self,
+        tasks: Sequence[RuntimeTask],
+    ) -> list[list[RuntimeTask]]:
+        """
+        Go through the tasks and assign each one to an employee.
+
+        Strategy:
+            - Assign first to idle workers evenly and randomly
+            - Employees with more idle workers get prioritized
+            - Assign remaining tasks to employees with the fewest tasks
+        """
+        # assignments will contain the assigned task list for each employee
+        assignments: list[list[RuntimeTask]] = [[] for _ in self.employees]
+
+        # Every employee's id repeated as many time as it has idle workers:
+        idle_id_repeated_list: list[int] = sum(
+            (
+                [i] * e.num_idle_workers
+                for i, e in enumerate(self.employees)
+            ), [],
+        )
+
+        # Shuffle to reduce chance of inefficiency described in handle_waiting
+        random.shuffle(idle_id_repeated_list)
+
+        # Assign tasks to idle workers in random order
+        for idle_employee_id, task in zip(idle_id_repeated_list, tasks):
+            assignments[idle_employee_id].append(task)
+
+        # Check if there are more tasks to be assigned
+        num_remaining_tasks = len(tasks) - len(idle_id_repeated_list)
+
+        if num_remaining_tasks <= 0:
+            return assignments
+
+        remaining_tasks = list(tasks[-num_remaining_tasks:])
+
+        # Sort the employees by how many tasks they have
+        ntasks = sorted([
+            (
+                e.num_tasks + len(assignments[i]),  # Consider idle assignments
+                random.random(),  # Random value for tie breaker
+                i,
+            )
+            for i, e in enumerate(self.employees)
+        ])
+
+        while len(remaining_tasks) > 0:
+            num_tasks, r, employee_id = ntasks[0]
+            assignments[employee_id].append(remaining_tasks.pop())
+            ntasks[0] = (num_tasks + 1, r, employee_id)
+
+            # Maintain sorted order by swapping up the updated count
+            idx = 0
+            while idx + 1 < len(ntasks) and ntasks[idx] > ntasks[idx + 1]:
+                ntasks[idx + 1], ntasks[idx] = ntasks[idx], ntasks[idx + 1]
+                idx += 1
+
+        return assignments
+
+    def schedule_tasks(self, tasks: Sequence[RuntimeTask]) -> None:
+        """Schedule tasks between this node's employees."""
+        if len(tasks) == 0:
+            return
+
+        assignments = self.assign_tasks(tasks)
+
+        for e, assignment in zip(self.employees, assignments):
+            num_tasks = len(assignment)
+
+            if num_tasks == 0:
+                continue
+
+            self.outgoing.put((e.conn, RuntimeMessage.SUBMIT_BATCH, assignment))
+
+            e.num_tasks += num_tasks
+            e.num_idle_workers -= min(num_tasks, e.num_idle_workers)
+
+        self.num_idle_workers = sum(e.num_idle_workers for e in self.employees)
+
+    def send_result_down(self, result: RuntimeResult) -> None:
+        """Send the `result` to the appropriate employee."""
+        dest_worker_id = result.return_address.worker_id
+
+        if not self.is_my_worker(dest_worker_id):
+            raise RuntimeError('Cannot send result to unmanaged worker.')
+
+        employee = self.get_employee_responsible_for(dest_worker_id)
+        self.outgoing.put((employee.conn, RuntimeMessage.RESULT, result))
+
+    def is_my_worker(self, worker_id: int) -> bool:
+        """Return true if `worker_id` is one of my workers (recursively)."""
+        employee_id = (worker_id - self.lower_id_bound) // self.step_size
+        return 0 <= employee_id < len(self.employees)
+
+    def get_employee_responsible_for(self, worker_id: int) -> RuntimeEmployee:
+        """Return the employee that manages `worker_id`."""
+        employee_id = (worker_id - self.lower_id_bound) // self.step_size
+        return self.employees[employee_id]
+
+    def broadcast_cancel(self, addr: RuntimeAddress) -> None:
+        """Broadcast a cancel message to my employees."""
+        for employee in self.employees:
+            self.outgoing.put((employee.conn, RuntimeMessage.CANCEL, addr))
+
+    def handle_waiting(self, conn: Connection, new_idle_count: int) -> None:
+        """
+        Record that an employee is idle with nothing to do.
+
+        There is a race condition here that is allowed. If an employee
+        sends a waiting message at the same time that this sends it a
+        task, it will still be marked waiting even though it is running
+        a task. We allow this for two reasons. First, the consequences are
+        minimal: this situation can only lead to one extra task assigned
+        to the worker that could otherwise go to a truly idle worker.
+        Second, it is unlikely in the common BQSKit workflows, which have
+        wide and shallow task graphs and each leaf task can require seconds
+        of runtime.
+        """
+        old_count = self.conn_to_employee_dict[conn].num_idle_workers
+        self.conn_to_employee_dict[conn].num_idle_workers = new_idle_count
+        self.num_idle_workers += (new_idle_count - old_count)
+        assert 0 <= self.num_idle_workers <= self.total_workers
+
+
+def parse_ipports(ipports_str: Sequence[str]) -> list[tuple[str, int]]:
+    """Parse command line ip and port inputs."""
+    ipports = []
+    for ipport_group in ipports_str:
+        for ipport in ipport_group.split(','):
+            if ipport.strip() == '':
+                continue
+            comps = ipport.strip().split(':')
+
+            if len(comps) == 1:
+                ip, port = comps[0], str(default_manager_port)
+                # Expect only managers to be listening on these ips
+                # so default port is manager's default port.
+
+            elif len(comps) == 2:
+                ip, port = comps
+
+            else:
+                raise ValueError(f'Invalid manager address: {ipport}.')
+
+            if not (0 <= int(port) < 65536):
+                raise ValueError(f'Invalid port number: {ipport}')
+
+            ipports.append((ip, int(port)))
+    return ipports
+
+
+def import_tests_package() -> None:
+    """
+    Import tests package recursively during detached architecture testing.
+
+    This should only be run by the CI test suite from the root bqskit folder.
+
+    credit: https://www.youtube.com/watch?v=t43zBsVcva0
+    """
+    sys.path.append(os.path.join(os.getcwd()))
+    import tests
+    import pkgutil
+    for mod in pkgutil.walk_packages(tests.__path__, f'{tests.__name__}.'):
+        __import__(mod.name, fromlist=['_trash'])
diff --git a/bqskit/runtime/detached.py b/bqskit/runtime/detached.py
new file mode 100644
index 000000000..80c44e7d8
--- /dev/null
+++ b/bqskit/runtime/detached.py
@@ -0,0 +1,428 @@
+"""This module implements the DetachedServer runtime."""
+from __future__ import annotations
+
+import argparse
+import logging
+import selectors
+import socket
+import time
+import uuid
+from dataclasses import dataclass
+from logging import LogRecord
+from multiprocessing.connection import Connection
+from multiprocessing.connection import Listener
+from threading import Thread
+from typing import Any
+from typing import cast
+from typing import List
+from typing import Sequence
+
+from bqskit.compiler.status import CompilationStatus
+from bqskit.compiler.task import CompilationTask
+from bqskit.runtime import default_server_port
+from bqskit.runtime.address import RuntimeAddress
+from bqskit.runtime.base import import_tests_package
+from bqskit.runtime.base import parse_ipports
+from bqskit.runtime.base import ServerBase
+from bqskit.runtime.direction import MessageDirection
+from bqskit.runtime.message import RuntimeMessage
+from bqskit.runtime.result import RuntimeResult
+from bqskit.runtime.task import RuntimeTask
+
+
+def listen(server: DetachedServer, port: int) -> None:
+    """Listening thread listens for client connections."""
+    listener = Listener(('0.0.0.0', port))
+    while server.running:
+        client = listener.accept()
+
+        if server.running:
+            # We check again that the server is running before registering
+            # the client because dummy data is sent to unblock
+            # listener.accept() during server shutdown
+            server.clients[client] = set()
+            server.sel.register(
+                client,
+                selectors.EVENT_READ,
+                MessageDirection.CLIENT,
+            )
+            server.logger.debug('Connected and registered new client.')
+
+    listener.close()
+
+
+@dataclass
+class ServerMailbox:
+    """A mailbox on a server is a final destination for a compilation task."""
+
+    result: Any = None
+    """Where the mailbox will store the result once it arrives."""
+
+    client_waiting: bool = False
+    """If true, the server knows a client is blocked waiting on this result."""
+
+    @property
+    def ready(self) -> bool:
+        """Return true if the mailbox result is ready and waiting delivery."""
+        return self.result is not None
+
+
+class DetachedServer(ServerBase):
+    """
+    BQSKit Runtime Server in detached mode.
+
+    In detached mode, the runtime is started separately from the client. Clients
+    can connect and disconnect while not shutting down the server. This
+    architecture is designed for the distributed setting, where managers manage
+    workers in shared memory and communicate with the server over a network.
+    """
+
+    def __init__(
+        self,
+        ipports: Sequence[tuple[str, int]],
+        port: int = default_server_port,
+    ) -> None:
+        """
+        Create a server and connect to the managers at `ipports`.
+
+        Args:
+            ipports (list[tuple[str, int]]): The ip and port pairs were
+                managers are expected to be listening for server connections.
+
+            port (int): The port this server will listen for clients on.
+                Default can be found in the
+                :obj:`~bqskit.runtime.default_server_port` global variable.
+        """
+        super().__init__()
+
+        self.clients: dict[Connection, set[uuid.UUID]] = {}
+        """Tracks all connected clients and all the tasks they have created."""
+
+        self.tasks: dict[uuid.UUID, tuple[int, Connection]] = {}
+        """Tracks all active CompilationTasks submitted to the cluster."""
+
+        self.mailbox_to_task_dict: dict[int, uuid.UUID] = {}
+        """Used to convert internal RuntimeTasks to client CompilationTasks."""
+
+        self.mailboxes: dict[int, ServerMailbox] = {}
+        """Mapping from mailbox ids to mailboxes."""
+
+        self.mailbox_counter = 0
+        """Counter to ensure all mailboxes have unique IDs."""
+
+        # Connect to managers
+        self.connect_to_managers(ipports)
+
+        # Start client listener
+        self.port = port
+        self.listen_thread = Thread(target=listen, args=(self, port))
+        self.listen_thread.start()
+        self.logger.info(f'Started client listener on port {self.port}.')
+
+    def handle_message(
+        self,
+        msg: RuntimeMessage,
+        direction: MessageDirection,
+        conn: Connection,
+        payload: Any,
+    ) -> None:
+        """Process the message coming from `direction`."""
+        if direction == MessageDirection.CLIENT:
+
+            if msg == RuntimeMessage.CONNECT:
+                pass
+
+            elif msg == RuntimeMessage.DISCONNECT:
+                self.handle_disconnect(conn)
+
+            elif msg == RuntimeMessage.SUBMIT:
+                ctask = cast(CompilationTask, payload)
+                self.handle_new_comp_task(conn, ctask)
+
+            elif msg == RuntimeMessage.REQUEST:
+                request = cast(uuid.UUID, payload)
+                self.handle_request(conn, request)
+
+            elif msg == RuntimeMessage.STATUS:
+                request = cast(uuid.UUID, payload)
+                self.handle_status(conn, request)
+
+            elif msg == RuntimeMessage.CANCEL:
+                request = cast(uuid.UUID, payload)
+                self.handle_cancel_comp_task(request)
+
+            else:
+                raise RuntimeError(f'Unexpected message type: {msg.name}')
+
+        elif direction == MessageDirection.BELOW:
+
+            if msg == RuntimeMessage.SUBMIT:
+                rtask = cast(RuntimeTask, payload)
+                self.schedule_tasks([rtask])
+
+            elif msg == RuntimeMessage.SUBMIT_BATCH:
+                rtasks = cast(List[RuntimeTask], payload)
+                self.schedule_tasks(rtasks)
+
+            elif msg == RuntimeMessage.RESULT:
+                result = cast(RuntimeResult, payload)
+                self.handle_result(result)
+
+            elif msg == RuntimeMessage.ERROR:
+                self.handle_error(payload)
+                return
+
+            elif msg == RuntimeMessage.LOG:
+                self.handle_log(payload)
+
+            elif msg == RuntimeMessage.CANCEL:
+                self.broadcast_cancel(payload)
+
+            elif msg == RuntimeMessage.SHUTDOWN:
+                self.handle_shutdown()
+
+            elif msg == RuntimeMessage.WAITING:
+                num_idle = cast(int, payload)
+                self.handle_waiting(conn, num_idle)
+
+            elif msg == RuntimeMessage.UPDATE:
+                task_diff = cast(int, payload)
+                self.conn_to_employee_dict[conn].num_tasks += task_diff
+
+            else:
+                raise RuntimeError(f'Unexpected message type: {msg.name}')
+
+        else:
+            raise RuntimeError(f'Unexpected message from {direction.name}.')
+
+    def handle_system_error(self, error_str: str) -> None:
+        """
+        Handle an error in runtime code as opposed to client code.
+
+        This is called when an error arises in runtime code not in a
+        RuntimeTask's coroutine code.
+        """
+        for client in self.clients.keys():
+            client.send((RuntimeMessage.ERROR, error_str))
+
+        # Sleep to ensure clients receive error message before shutdown
+        time.sleep(1)
+
+    def handle_shutdown(self) -> None:
+        """Shutdown the runtime."""
+        super().handle_shutdown()
+
+        # Close client connections
+        for client in self.clients.keys():
+            try:
+                client.close()
+            except Exception:
+                pass
+        self.clients.clear()
+        self.logger.debug('Cleared clients.')
+
+        # Close listener (hasattr checked for attachedserver shutdown)
+        if hasattr(self, 'listen_thread') and self.listen_thread.is_alive():
+            # Listener will be blocked on the accept call
+            # Create a dummy connection to unblock it
+            # Workaround credit: https://stackoverflow.com/questions/16734534
+            # Related bug: https://github.com/python/cpython/issues/76425
+            dummy_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            dummy_socket.connect(('localhost', self.port))
+            dummy_socket.close()
+            self.listen_thread.join()
+            self.logger.debug('Joined listening thread.')
+
+    def handle_disconnect(self, conn: Connection) -> None:
+        """Disconnect a client connection from the runtime."""
+        super().handle_disconnect(conn)
+        tasks = self.clients.pop(conn)
+        for task_id in tasks:
+            self.handle_cancel_comp_task(task_id)
+        self.logger.info('Unregistered client.')
+
+    def handle_new_comp_task(
+        self,
+        conn: Connection,
+        task: CompilationTask,
+    ) -> None:
+        """Convert a :class:`CompilationTask` into an internal one."""
+        mailbox_id = self._get_new_mailbox_id()
+        self.tasks[task.task_id] = (mailbox_id, conn)
+        self.mailbox_to_task_dict[mailbox_id] = task.task_id
+        self.mailboxes[mailbox_id] = ServerMailbox()
+        self.logger.info(f'New CompilationTask: {task.task_id}.')
+
+        self.clients[conn].add(task.task_id)
+
+        internal_task = RuntimeTask(
+            (CompilationTask.run, (task,), {}),
+            RuntimeAddress(-1, mailbox_id, 0),
+            mailbox_id,
+            tuple(),
+            task.logging_level,
+            task.max_logging_depth,
+        )
+
+        self.schedule_tasks([internal_task])
+
+    def handle_request(self, conn: Connection, request: uuid.UUID) -> None:
+        """Record the requested task, and ship it as soon as it's ready."""
+        if request not in self.clients[conn] or request not in self.tasks:
+            self.outgoing.put((conn, RuntimeMessage.ERROR, 'Unknown task.'))
+            self.handle_disconnect(conn)  # Bad client
+            return
+
+        # Get the mailbox associated with this task.
+        mailbox_id = self.tasks[request][0]
+        box = self.mailboxes[mailbox_id]
+
+        if box.ready:
+            # If the result has already arrived, ship it to the client.
+            self.logger.info(f'Responding to request for task {request}.')
+            self.outgoing.put((conn, RuntimeMessage.RESULT, box.result))
+            self.mailboxes.pop(mailbox_id)
+            self.clients[conn].remove(request)
+            # t_id is not removed from self.tasks or
+            # self.mailbox_to_task_dict incase there are left
+            # over log messages arriving.
+
+        else:
+            # Otherwise, note that the client is waiting on this result.
+            # This is so the result will be shipped the instant it arrives.
+            box.client_waiting = True
+
+    def handle_status(self, conn: Connection, request: uuid.UUID) -> None:
+        """Inform the client if the task is finished or not."""
+        if request not in self.clients[conn] or request not in self.tasks:
+            # This task is unknown to the system
+            m = (conn, RuntimeMessage.STATUS, CompilationStatus.UNKNOWN)
+            self.outgoing.put(m)
+
+        # Get the mailbox associated with this task.
+        mailbox_id = self.tasks[request][0]
+        box = self.mailboxes[mailbox_id]
+
+        # Send status
+        s = CompilationStatus.DONE if box.ready else CompilationStatus.RUNNING
+        self.outgoing.put((conn, RuntimeMessage.STATUS, s))
+
+    def handle_cancel_comp_task(self, request: uuid.UUID) -> None:
+        """Cancel a compilation task in the system."""
+        self.logger.info(f'Cancelling: {request}.')
+
+        # Remove task from server data
+        mailbox_id, client_conn = self.tasks[request]
+        self.mailboxes.pop(mailbox_id)
+        if client_conn in self.clients:
+            self.clients[client_conn].remove(request)
+
+        # Forward internal cancel messages
+        addr = RuntimeAddress(-1, mailbox_id, 0)
+        self.broadcast_cancel(addr)
+
+        # Acknowledge the client's cancel request
+        if not client_conn.closed:
+            # Check if it closed first since the client may have disconnected
+            self.outgoing.put((client_conn, RuntimeMessage.CANCEL, None))
+
+    def handle_result(self, result: RuntimeResult) -> None:
+        """Either store the result here or ship it to the destination worker."""
+        # Record a task has been completed
+        self.get_employee_responsible_for(result.completed_by).num_tasks -= 1
+
+        # Check if the result is for a client
+        if result.return_address.worker_id == -1:
+            mailbox_id = result.return_address.mailbox_index
+            if mailbox_id not in self.mailboxes:
+                return  # Silently discard results from cancelled tasks
+
+            box = self.mailboxes[mailbox_id]
+            box.result = result.result
+            t_id = self.mailbox_to_task_dict[mailbox_id]
+            self.logger.info(f'Finished: {t_id}.')
+
+            if box.client_waiting:
+                self.logger.info(f'Responding to request for task {t_id}.')
+                m = (self.tasks[t_id][1], RuntimeMessage.RESULT, box.result)
+                self.outgoing.put(m)
+                self.clients[self.tasks[t_id][1]].remove(t_id)
+                self.mailboxes.pop(mailbox_id)
+                # t_id is not removed from self.tasks or
+                # self.mailbox_to_task_dict incase there are left
+                # over log messages arriving.
+
+        else:
+            self.send_result_down(result)
+
+    def handle_error(self, error_payload: tuple[int, str]) -> None:
+        """Forward an error to the appropriate client and disconnect it."""
+        if not isinstance(error_payload, tuple):
+            # Internal errors may bubble up without a task_id
+            assert isinstance(error_payload, str)
+            self.handle_system_error(error_payload)
+            self.handle_shutdown()
+            raise RuntimeError(error_payload)
+
+        tid = error_payload[0]
+        conn = self.tasks[self.mailbox_to_task_dict[tid]][1]
+        self.outgoing.put((conn, RuntimeMessage.ERROR, error_payload[1]))
+
+    def handle_log(self, log_payload: tuple[int, LogRecord]) -> None:
+        """Forward logs to appropriate client."""
+        tid = log_payload[0]
+        conn = self.tasks[self.mailbox_to_task_dict[tid]][1]
+        self.outgoing.put((conn, RuntimeMessage.LOG, log_payload[1]))
+
+    def _get_new_mailbox_id(self) -> int:
+        """Unique mailbox id counter."""
+        new_id = self.mailbox_counter
+        self.mailbox_counter += 1
+        return new_id
+
+
+def start_server() -> None:
+    """Entry point for a detached runtime server process."""
+    parser = argparse.ArgumentParser(
+        prog='BQSKit Server',
+        description='Launch a BQSKit runtime server process.',
+    )
+    parser.add_argument(
+        'managers',
+        nargs='+',
+        help='The ip and port pairs were managers are expected to be waiting.',
+    )
+    parser.add_argument(
+        '-p', '--port',
+        type=int,
+        default=default_server_port,
+        help='The port this server will listen for clients on.',
+    )
+    parser.add_argument(
+        '--verbose', '-v',
+        action='count',
+        default=0,
+        help='Enable logging of increasing verbosity, either -v, -vv, or -vvv.',
+    )
+    parser.add_argument(
+        '--import-tests', '-i',
+        action='store_true',
+        help='Import the bqskit tests package; used during testing.',
+    )
+    args = parser.parse_args()
+    ipports = parse_ipports(args.managers)
+
+    # Set up logging
+    _logger = logging.getLogger('bqskit-runtime')
+    _logger.setLevel([30, 20, 10, 1][min(args.verbose, 3)])
+    _logger.addHandler(logging.StreamHandler())
+
+    # Import tests package recursively
+    if args.import_tests:
+        import_tests_package()
+
+    # Create the server
+    server = DetachedServer(ipports, args.port)
+
+    # Start the server
+    server.run()
diff --git a/bqskit/runtime/direction.py b/bqskit/runtime/direction.py
new file mode 100644
index 000000000..affffda5e
--- /dev/null
+++ b/bqskit/runtime/direction.py
@@ -0,0 +1,20 @@
+"""This module implements the MessageDirection enum."""
+from __future__ import annotations
+
+from enum import IntEnum
+
+
+class MessageDirection(IntEnum):
+    """The direction a message came from."""
+
+    BELOW = 0
+    """This describes messages coming from a child manager/worker."""
+
+    ABOVE = 1
+    """This describes messages coming from a parent server/manager."""
+
+    CLIENT = 2
+    """This describes messages coming from a client."""
+
+    SIGNAL = 3
+    """This describes internal directives created by a signal handler."""
diff --git a/bqskit/runtime/future.py b/bqskit/runtime/future.py
new file mode 100644
index 000000000..70f6ac2cc
--- /dev/null
+++ b/bqskit/runtime/future.py
@@ -0,0 +1,56 @@
+"""This module implements the RuntimeFuture class."""
+from __future__ import annotations
+
+from typing import Any
+
+
+class RuntimeFuture:
+    """
+    An awaitable future.
+
+    These future objects must be awaited in the same task that created them.
+    They cannot be used as input to other tasks.
+    """
+
+    def __init__(self, mailbox_id: int) -> None:
+        """Initialize a future tied to a local mailbox."""
+
+        self.mailbox_id = mailbox_id
+        """The mailbox id where this future's result will be shipped."""
+
+        self._next_flag = False
+        """If flag is set, then awaiting returns next result."""
+
+    def __await__(self) -> Any:
+        """
+        Wait on a result to be delivered.
+
+        Informs the event loop which mailbox this is waiting on.
+        """
+        if self._next_flag:
+            return (yield self)
+
+        return (yield self)
+
+    def __getstate__(self) -> Any:
+        """Prevent a future from being sent to another process."""
+        raise NotImplementedError(
+            'These future objects must be awaited in the same task that'
+            ' created them. They cannot be inputted to other tasks.',
+        )
+
+    @property
+    def _done(self) -> bool:
+        """
+        Return true if the future is ready.
+
+        Warning: Polling on futures in a busy-wait loop can cause deadlock.
+        It is best to await on futures rather than continuously polling.
+        Use at your own risk.
+        """
+        from bqskit.runtime.worker import get_worker
+
+        if self.mailbox_id not in get_worker()._mailboxes:
+            return True
+
+        return get_worker()._mailboxes[self.mailbox_id].ready
diff --git a/bqskit/runtime/manager.py b/bqskit/runtime/manager.py
new file mode 100644
index 000000000..6155a5961
--- /dev/null
+++ b/bqskit/runtime/manager.py
@@ -0,0 +1,308 @@
+"""This module implements the Manager class."""
+from __future__ import annotations
+
+import argparse
+import logging
+import selectors
+import time
+from multiprocessing.connection import Connection
+from typing import Any
+from typing import cast
+from typing import List
+from typing import Sequence
+
+from bqskit.runtime import default_manager_port
+from bqskit.runtime import default_worker_port
+from bqskit.runtime.address import RuntimeAddress
+from bqskit.runtime.base import import_tests_package
+from bqskit.runtime.base import parse_ipports
+from bqskit.runtime.base import ServerBase
+from bqskit.runtime.direction import MessageDirection
+from bqskit.runtime.message import RuntimeMessage
+from bqskit.runtime.result import RuntimeResult
+from bqskit.runtime.task import RuntimeTask
+
+
+class Manager(ServerBase):
+    """
+    BQSKit Runtime Manager.
+
+    A Manager is a middle node in the process hierarchy and is responsible for
+    managing workers or other managers. The manager is part of the detached
+    architecture. Here managers are started individually as separate processes,
+    which in turn start their own workers. Then, if necessary, more managers can
+    be started to manage the level-1 managers, and so on, until finally, a
+    detached server is started and clients can connect.
+    """
+
+    def __init__(
+        self,
+        port: int = default_manager_port,
+        num_workers: int = -1,
+        ipports: list[tuple[str, int]] | None = None,
+        worker_port: int = default_worker_port,
+    ) -> None:
+        """
+        Create a manager instance in one of two ways:
+
+        1) Leave all options default and start a manager which spawns and
+            manages as many worker processes as there are os threads.
+            You can also specify the number of workers to spawn via
+            the `num_workers` parameter. In this mode, the manager
+            is a level-1 manager and manages workers.
+
+        2) Specify ip and port pairs, then assume at each endpoint there
+            is a listening manager and attempt to establish a connection.
+            In this mode, the manager will not spawn any workers and just
+            manage the specified managers.
+
+        In either case, if any problems arise during startup, no recovery
+        is attempted and the manager terminates.
+
+        Args:
+            port (int): The port this manager listens for server connections.
+                Default can be found in the
+                :obj:`~bqskit.runtime.default_manager_port` global variable.
+
+            num_workers (int): The number of workers to spawn. If -1,
+                then spawn as many workers as CPUs on the system.
+                (Default: -1). Ignored if `ipports` is not None.
+
+            ipports (list[tuple[str, int]] | None): If not None, then all
+                the addresses and ports of running managers to connect to.
+
+            worker_port (int): The port this server will listen for workers
+                on. Default can be found in the
+                :obj:`~bqskit.runtime.default_worker_port` global variable.
+        """
+        super().__init__()
+
+        # Connect upstream
+        self.upstream = self.listen_once(port)
+
+        # Handshake with upstream
+        msg, payload = self.upstream.recv()
+        assert msg == RuntimeMessage.CONNECT
+        self.lower_id_bound = payload[0]
+        self.upper_id_bound = payload[1]
+        self.sel.register(
+            self.upstream,
+            selectors.EVENT_READ,
+            MessageDirection.ABOVE,
+        )
+
+        # Case 1: spawn and manage workers
+        if ipports is None:
+            self.spawn_workers(num_workers, worker_port)
+
+        # Case 2: Connect to managers at ipports
+        else:
+            self.connect_to_managers(ipports)
+
+        # Track info on sent messages to reduce redundant messages:
+        self.last_num_idle_sent_up = self.total_workers
+
+        # Inform upstream we are starting
+        msg = (self.upstream, RuntimeMessage.STARTED, self.total_workers)
+        self.outgoing.put(msg)
+        self.logger.info('Sent start message upstream.')
+
+    def handle_message(
+        self,
+        msg: RuntimeMessage,
+        direction: MessageDirection,
+        conn: Connection,
+        payload: Any,
+    ) -> None:
+        """Process the message coming from `direction`."""
+        if direction == MessageDirection.ABOVE:
+
+            if msg == RuntimeMessage.SUBMIT:
+                rtask = cast(RuntimeTask, payload)
+                self.schedule_tasks([rtask])
+                self.update_upstream_idle_workers()
+
+            elif msg == RuntimeMessage.SUBMIT_BATCH:
+                rtasks = cast(List[RuntimeTask], payload)
+                self.schedule_tasks(rtasks)
+                self.update_upstream_idle_workers()
+
+            elif msg == RuntimeMessage.RESULT:
+                result = cast(RuntimeResult, payload)
+                self.send_result_down(result)
+
+            elif msg == RuntimeMessage.CANCEL:
+                addr = cast(RuntimeAddress, payload)
+                self.broadcast_cancel(addr)
+
+            elif msg == RuntimeMessage.SHUTDOWN:
+                self.handle_shutdown()
+
+            else:
+                raise RuntimeError(f'Unexpected message type: {msg.name}')
+
+        elif direction == MessageDirection.BELOW:
+
+            if msg == RuntimeMessage.SUBMIT:
+                rtask = cast(RuntimeTask, payload)
+                self.send_up_or_schedule_tasks([rtask])
+                self.update_upstream_idle_workers()
+
+            elif msg == RuntimeMessage.SUBMIT_BATCH:
+                rtasks = cast(List[RuntimeTask], payload)
+                self.send_up_or_schedule_tasks(rtasks)
+                self.update_upstream_idle_workers()
+
+            elif msg == RuntimeMessage.RESULT:
+                result = cast(RuntimeResult, payload)
+                self.handle_result_from_below(result)
+
+            elif msg == RuntimeMessage.WAITING:
+                num_idle = cast(int, payload)
+                self.handle_waiting(conn, num_idle)
+                self.update_upstream_idle_workers()
+
+            elif msg == RuntimeMessage.UPDATE:
+                task_diff = cast(int, payload)
+                self.handle_update(conn, task_diff)
+
+            else:
+                # Forward all other messages up
+                self.outgoing.put((self.upstream, msg, payload))
+
+        else:
+            raise RuntimeError(f'Unexpected message from {direction.name}.')
+
+    def handle_system_error(self, error_str: str) -> None:
+        """
+        Handle an error in runtime code as opposed to client code.
+
+        This is called when an error arises in runtime code not in a
+        RuntimeTask's coroutine code.
+        """
+        try:
+            self.upstream.send((RuntimeMessage.ERROR, error_str))
+
+            # Sleep to ensure server receives error message before shutdown
+            time.sleep(1)
+
+        except Exception:
+            # If server has crashed then just exit
+            pass
+
+    def handle_shutdown(self) -> None:
+        """Shutdown the manager and clean up spawned processes."""
+        super().handle_shutdown()
+
+        # Forward shutdown message upwards if possible
+        try:
+            self.upstream.send((RuntimeMessage.SHUTDOWN, None))
+            self.upstream.close()
+
+        except Exception:
+            # If server has already shutdown or crashed, just exit
+            pass
+
+    def send_up_or_schedule_tasks(self, tasks: Sequence[RuntimeTask]) -> None:
+        """Either send the tasks upstream or schedule them downstream."""
+        num_idle = self.num_idle_workers
+
+        if num_idle != 0:
+            self.outgoing.put((self.upstream, RuntimeMessage.UPDATE, num_idle))
+            self.schedule_tasks(tasks[:num_idle])
+
+        if len(tasks) > num_idle:
+            self.outgoing.put((
+                self.upstream,
+                RuntimeMessage.SUBMIT_BATCH,
+                tasks[num_idle:],
+            ))
+
+    def handle_result_from_below(self, result: RuntimeResult) -> None:
+        """Forward the result to its destination and track the completion."""
+        # Record a task has been completed
+        self.get_employee_responsible_for(result.completed_by).num_tasks -= 1
+
+        # Forward result to final destination
+        if self.is_my_worker(result.return_address.worker_id):
+            self.send_result_down(result)
+            self.outgoing.put((self.upstream, RuntimeMessage.UPDATE, -1))
+
+        else:
+            # If its destination worker is not an employee of mine,
+            # then my boss will know where to send this result.
+            self.outgoing.put((self.upstream, RuntimeMessage.RESULT, result))
+
+    def update_upstream_idle_workers(self) -> None:
+        """Update the total number of idle workers upstream."""
+        if self.num_idle_workers != self.last_num_idle_sent_up:
+            self.last_num_idle_sent_up = self.num_idle_workers
+            m = (self.upstream, RuntimeMessage.WAITING, self.num_idle_workers)
+            self.outgoing.put(m)
+
+    def handle_update(self, conn: Connection, task_diff: int) -> None:
+        """Handle a task count update from a lower level manager or worker."""
+        self.conn_to_employee_dict[conn].num_tasks += task_diff
+        self.outgoing.put((self.upstream, RuntimeMessage.UPDATE, task_diff))
+
+
+def start_manager() -> None:
+    """Entry point for runtime manager processes."""
+    parser = argparse.ArgumentParser(
+        prog='BQSKit Manager',
+        description='Launch a BQSKit runtime manager process.',
+    )
+    parser.add_argument(
+        '-n', '--num-workers',
+        default=-1,
+        type=int,
+        help='The number of workers to spawn. If negative, will spawn'
+        ' one worker for each available CPU. Defaults to -1.',
+    )
+    parser.add_argument(
+        '-m', '--managers',
+        nargs='+',
+        help='The ip and port pairs were managers are expected to be waiting.',
+    )
+    parser.add_argument(
+        '-p', '--port',
+        type=int,
+        default=default_manager_port,
+        help='The port this manager will listen for servers on.',
+    )
+    parser.add_argument(
+        '-w', '--worker-port',
+        type=int,
+        default=default_worker_port,
+        help='The port this manager will listen for workers on.',
+    )
+    parser.add_argument(
+        '--verbose', '-v',
+        action='count',
+        default=0,
+        help='Enable logging of increasing verbosity, either -v, -vv, or -vvv.',
+    )
+    parser.add_argument(
+        '--import-tests', '-i',
+        action='store_true',
+        help='Import the bqskit tests package; used during testing.',
+    )
+    args = parser.parse_args()
+
+    # If ips and ports were provided parse them
+    ipports = None if args.managers is None else parse_ipports(args.managers)
+
+    # Set up logging
+    _logger = logging.getLogger('bqskit-runtime')
+    _logger.setLevel([30, 20, 10, 1][min(args.verbose, 3)])
+    _logger.addHandler(logging.StreamHandler())
+
+    # Import tests package recursively
+    if args.import_tests:
+        import_tests_package()
+
+    # Create the manager
+    manager = Manager(args.port, args.num_workers, ipports, args.worker_port)
+
+    # Start the manager
+    manager.run()
diff --git a/bqskit/runtime/message.py b/bqskit/runtime/message.py
new file mode 100644
index 000000000..63f687048
--- /dev/null
+++ b/bqskit/runtime/message.py
@@ -0,0 +1,22 @@
+"""This module implements the RuntimeMessage enum."""
+from __future__ import annotations
+
+from enum import IntEnum
+
+
+class RuntimeMessage(IntEnum):
+    """An message sent between processes in the BQSKit runtime."""
+    CONNECT = 0
+    DISCONNECT = 1
+    STARTED = 2
+    SHUTDOWN = 3
+    ERROR = 4
+    REQUEST = 5
+    RESULT = 6
+    SUBMIT = 7
+    SUBMIT_BATCH = 8
+    STATUS = 9
+    LOG = 10
+    CANCEL = 11
+    WAITING = 12
+    UPDATE = 13
diff --git a/bqskit/runtime/result.py b/bqskit/runtime/result.py
new file mode 100644
index 000000000..9dc2725d8
--- /dev/null
+++ b/bqskit/runtime/result.py
@@ -0,0 +1,14 @@
+"""This module implements the RuntimeResult NamedTuple."""
+from __future__ import annotations
+
+from typing import Any
+from typing import NamedTuple
+
+from bqskit.runtime.address import RuntimeAddress
+
+
+class RuntimeResult(NamedTuple):
+    """The result of a task, ready to be shipped to its destination."""
+    return_address: RuntimeAddress
+    result: Any
+    completed_by: int
diff --git a/bqskit/runtime/task.py b/bqskit/runtime/task.py
new file mode 100644
index 000000000..b55037a87
--- /dev/null
+++ b/bqskit/runtime/task.py
@@ -0,0 +1,112 @@
+"""This module implements the RuntimeTask class."""
+from __future__ import annotations
+
+import inspect
+import logging
+from typing import Any
+from typing import Coroutine
+
+from bqskit.runtime.address import RuntimeAddress
+
+
+class RuntimeTask:
+    """
+    BQSKit Runtime's internal task structure.
+
+    The task exists in two phases:
+        1. In the initial phase, the task is a context-less function
+        and arguments grouped with a return address. It is expected
+        when the task is finished executing, to ship the result to
+        the return address.
+
+        2. When the task has been scheduled to run on a worker, it is
+        initialized into a coroutine that can be executed in steps.
+    """
+
+    task_counter = 0
+    """Per-process task id counter."""
+
+    def __init__(
+        self,
+        fnargs: tuple[Any, Any, Any],
+        return_address: RuntimeAddress,
+        comp_task_id: int,
+        breadcrumbs: tuple[RuntimeAddress, ...],
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> None:
+        """Create the task with a new id and return address."""
+        RuntimeTask.task_counter += 1
+        self.task_id = RuntimeTask.task_counter
+
+        self.fnargs = fnargs
+        """Tuple of function pointer, arguments, and keyword arguments."""
+
+        self.return_address = return_address
+        """Where the result of this task should be sent."""
+
+        self.logging_level = logging_level
+        """Logs with levels >= to this get emitted, if None always emit."""
+
+        self.comp_task_id = comp_task_id
+        """The mailbox id of the this task's root task."""
+
+        self.breadcrumbs = breadcrumbs
+        """All of this task's parent tasks' addresses in order."""
+
+        self.max_logging_depth = max_logging_depth
+        """Logs are not emitted for tasks with this many or more parents."""
+
+        self.coro: Coroutine[Any, Any, Any] | None = None
+        """The coroutine containing this tasks code."""
+
+        # self.send: Any = None
+        # """A register that both the coroutine and task have access to."""
+
+        self.desired_box_id: int | None = None
+        """When waiting on a mailbox, this stores that mailbox's id."""
+
+        self.owned_mailboxes: list[int] = []
+        """The mailbox ids that this task owns."""
+
+        self.wake_on_next: bool = False
+        """Set to true if this task should wake immediately on a result."""
+
+    def step(self, send_val: Any = None) -> Any:
+        """Execute one step of the task."""
+        if self.coro is None:
+            raise RuntimeError('Task has not been initialized.')
+
+        # Reset previously set await flags
+        self.wake_on_next = False
+        self.desired_box_id = None
+
+        # Set logging level
+        old_level = logging.getLogger().getEffectiveLevel()
+        if (
+            self.max_logging_depth < 0
+            or len(self.breadcrumbs) <= self.max_logging_depth
+        ):
+            logging.getLogger().setLevel(0)
+
+        # Execute a task step
+        to_return = self.coro.send(send_val)
+
+        # Reset logging
+        logging.getLogger().setLevel(old_level)
+
+        return to_return
+
+    def start(self) -> None:
+        """Initialize the task."""
+        self.coro = self.run()
+
+    async def run(self) -> Any:
+        """Task coroutine wrapper."""
+        if inspect.iscoroutinefunction(self.fnargs[0]):
+            return await self.fnargs[0](*self.fnargs[1], **self.fnargs[2])
+        return self.fnargs[0](*self.fnargs[1], **self.fnargs[2])
+
+    def is_descendant_of(self, addr: RuntimeAddress) -> bool:
+        """Return true if `addr` identifies a parent (or this) task."""
+        return addr == self.return_address or addr in self.breadcrumbs
diff --git a/bqskit/runtime/worker.py b/bqskit/runtime/worker.py
new file mode 100644
index 000000000..af90635a7
--- /dev/null
+++ b/bqskit/runtime/worker.py
@@ -0,0 +1,623 @@
+"""This module implements BQSKit Runtime's Worker."""
+from __future__ import annotations
+
+import logging
+import signal
+import sys
+import time
+import traceback
+from collections import OrderedDict
+from dataclasses import dataclass
+from multiprocessing.connection import Client
+from multiprocessing.connection import Connection
+from multiprocessing.connection import wait
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import List
+
+from bqskit.runtime.address import RuntimeAddress
+from bqskit.runtime.future import RuntimeFuture
+from bqskit.runtime.message import RuntimeMessage
+from bqskit.runtime.result import RuntimeResult
+from bqskit.runtime.task import RuntimeTask
+
+
+class WorkerQueue():
+    """The worker's task FIFO queue."""
+
+    def __init__(self) -> None:
+        """
+        Initialize the worker queue.
+
+        An OrderedDict is used to internally store the task. This prevents the
+        same task appearing multiple times in the queue, while also ensuring
+        O(1) operations.
+        """
+        self._queue: OrderedDict[RuntimeAddress, None] = OrderedDict()
+
+    def put(self, addr: RuntimeAddress) -> None:
+        """Enqueue a task by its address."""
+        if addr not in self._queue:
+            self._queue[addr] = None
+
+    def get(self) -> RuntimeAddress:
+        """Get the next task to run."""
+        return self._queue.popitem(last=False)[0]
+
+    def empty(self) -> bool:
+        """Check if the queue is empty."""
+        return len(self._queue) == 0
+
+
+@dataclass
+class WorkerMailbox:
+    """
+    A mailbox on a worker is a final destination for a task's result.
+
+    When a task is created, a mailbox is also created with an associated future.
+    The parent task can await on the future, letting the worker's event loop
+    know it is waiting on the associated result. When a result arrives, it is
+    placed in the appropriate mailbox and the waiting task is placed into the
+    ready queue.
+    """
+    expecting_single_result: bool = False
+    expected_num_results: int = 0
+    result: Any = None
+    num_results: int = 0
+    dest_addr: RuntimeAddress | None = None
+    fresh_results: list[Any] | None = None
+
+    @property
+    def ready(self) -> bool:
+        """Return true if the mailbox has all expected results."""
+        return (
+            self.num_results >= self.expected_num_results
+            and self.num_results != 0
+        )
+
+    @property
+    def has_task_waiting(self) -> bool:
+        """Return True if a task is waiting on the result of this box."""
+        return self.dest_addr is not None
+
+    @staticmethod
+    def new_mailbox(num_results: int | None = None) -> WorkerMailbox:
+        """
+        Create a new mailbox with `num_results` slots.
+
+        If `num_results` is None (by default), then the mailbox will only have
+        one slot and expect one result.
+        """
+        if num_results is None:
+            return WorkerMailbox(True, 1)
+
+        return WorkerMailbox(False, num_results, [None] * num_results)
+
+    def get_new_results(self) -> list[tuple[int, Any]]:
+        """Return and reset the results that have come in since previous
+        call."""
+        assert self.fresh_results is not None
+        out = self.fresh_results
+        self.fresh_results = []
+        return out
+
+    def deposit_result(self, result: RuntimeResult) -> None:
+        """Store the result in the mailbox."""
+        self.num_results += 1
+        slot_id = result.return_address.mailbox_slot
+
+        # Record as fresh result
+        if self.fresh_results is None:
+            self.fresh_results = []
+        self.fresh_results.append((slot_id, result.result))
+
+        if self.expecting_single_result:
+            self.result = result.result
+        else:
+            self.result[slot_id] = result.result
+
+
+class Worker:
+    """
+    BQSKit Runtime's Worker.
+
+    BQSKit Runtime utilizes a single-threaded worker to accept, execute,
+    pause, spawn, resume, and complete tasks in a custom event loop built
+    with python's async await mechanisms. Each worker receives and sends
+    tasks and results to the greater system through a single duplex
+    connection with a runtime server or manager.
+
+    At start-up, the worker receives an ID and waits for its first task.
+    An executing task may use the `submit` and `map` methods to spawn child
+    tasks and distribute them across the whole system. Once completed,
+    those child tasks will have their results shipped back to the worker
+    who created them. When a task awaits a child task, it is removed from
+    the ready queue until the desired results come in.
+
+    All created log records are shipped back to the client's process.
+    This feature ensures compatibility with applications like jupyter
+    that only print messages from the client process's stdout. Additionally,
+    it allows BQSKit users seamless integration with the standard python
+    logging module. From a user's perspective, they can configure any
+    standard python logger from their process like usual and have the
+    entire system honor that configuration. Lastly, we do support an
+    additional logging option for maximum task depth. Tasks with more
+    ancestors than the maximum logging depth will not produce any logs.
+
+    Workers handle python errors by capturing and bubbling them up. For
+    operating system-level crashes and errors -- such as seg-faults in
+    client code -- the worker will attempt to print a stack trace and
+    initiate a system-wide shutdown. However, note that these issues
+    can go unhandled and cause the runtime to deadlock or crash.
+
+    Workers perform very minimal scheduling of tasks. Newly created tasks
+    are directly forwarded upwards to a manager or server, which in turn
+    assigns them to workers. New tasks from above are commonly received
+    in batches. In this case, all but one task from a batch is delayed.
+    Delayed tasks are moved (in LIFO order) to the ready queue if a worker
+    has no other work to complete. This mechanism encourages completing
+    deeply-nested tasks first and prevents flooding the system with active
+    tasks, which usually require much more memory than delayed ones.
+    """
+
+    def __init__(self, id: int, conn: Connection) -> None:
+        """
+        Initialize a worker with no tasks.
+
+        Args:
+            id (int): This worker's id.
+
+            conn (Connection): This worker's duplex channel to a manager
+                or a server.
+        """
+        self._id = id
+        self._conn = conn
+
+        self._outgoing: list[tuple[RuntimeMessage, Any]] = []
+        """Stores outgoing messages to be handled by the event loop."""
+
+        self._tasks: dict[RuntimeAddress, RuntimeTask] = {}
+        """Tracks all started, unfinished tasks on this worker."""
+
+        self._delayed_tasks: list[RuntimeTask] = []
+        """Store all delayed tasks in LIFO order."""
+
+        self._ready_task_ids: WorkerQueue = WorkerQueue()
+        """Tasks queued up for execution."""
+
+        self._cancelled_task_ids: set[RuntimeAddress] = set()
+        """To ensure newly-received cancelled tasks are never started."""
+
+        self._active_task: RuntimeTask | None = None
+        """The currently executing task if one is running."""
+
+        self._running = False
+        """Controls if the event loop is running."""
+
+        self._mailboxes: dict[int, WorkerMailbox] = {}
+        """Map from mailbox ids to worker mailboxes."""
+
+        self._mailbox_counter = 0
+        """This count ensures every mailbox has a unique id."""
+
+        # Send out every emitted log message upstream
+        old_factory = logging.getLogRecordFactory()
+
+        def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
+            record = old_factory(*args, **kwargs)
+            active_task = get_worker()._active_task
+            if active_task is not None:
+                lvl = active_task.logging_level
+                if lvl is None or lvl <= record.levelno:
+                    tid = active_task.comp_task_id
+                    self._outgoing.append((RuntimeMessage.LOG, (tid, record)))
+            return record
+
+        logging.setLogRecordFactory(record_factory)
+
+        # Communicate that this worker is ready
+        self._conn.send((RuntimeMessage.STARTED, self._id))
+
+    def _loop(self) -> None:
+        """Main worker event loop."""
+        self._running = True
+        while self._running:
+            self._try_step_next_ready_task()
+            self._try_idle()
+            self._handle_comms()
+
+    def _try_idle(self) -> None:
+        """If there is nothing to do, wait until we receive a message."""
+        empty_outgoing = len(self._outgoing) == 0
+        no_ready_tasks = self._ready_task_ids.empty()
+        no_delayed_tasks = len(self._delayed_tasks) == 0
+
+        if empty_outgoing and no_ready_tasks and no_delayed_tasks:
+            self._conn.send((RuntimeMessage.WAITING, 1))
+            wait([self._conn])
+
+    def _handle_comms(self) -> None:
+        """Handle all incoming and outgoing messages."""
+
+        # Handle outgoing communication
+        for out_msg in self._outgoing:
+            self._conn.send(out_msg)
+        self._outgoing.clear()
+
+        # Handle incomming communication
+        while self._conn.poll():
+            msg, payload = self._conn.recv()
+
+            # Process message
+            if msg == RuntimeMessage.SHUTDOWN:
+                self._running = False
+                return
+
+            elif msg == RuntimeMessage.SUBMIT:
+                task = cast(RuntimeTask, payload)
+                self._add_task(task)
+
+            elif msg == RuntimeMessage.SUBMIT_BATCH:
+                tasks = cast(List[RuntimeTask], payload)
+                self._add_task(tasks.pop())  # Submit one task
+                self._delayed_tasks.extend(tasks)  # Delay rest
+                # Delayed tasks have no context and are stored (more-or-less)
+                # as a function pointer together with the arguments.
+                # When it gets started, it consumes much more memory,
+                # so we delay the task start until necessary (at no cost)
+
+            elif msg == RuntimeMessage.RESULT:
+                result = cast(RuntimeResult, payload)
+                self._handle_result(result)
+
+            elif msg == RuntimeMessage.CANCEL:
+                addr = cast(RuntimeAddress, payload)
+                self._handle_cancel(addr)
+
+    def _add_task(self, task: RuntimeTask) -> None:
+        """Start a task and add it to the loop."""
+        self._tasks[task.return_address] = task
+        task.start()
+        self._ready_task_ids.put(task.return_address)
+
+    def _handle_result(self, result: RuntimeResult) -> None:
+        """Insert result into appropriate mailbox and wake waiting task."""
+        mailbox_id = result.return_address.mailbox_index
+        assert result.return_address.worker_id == self._id
+        if mailbox_id not in self._mailboxes:
+            # If the mailbox has been dropped due to a cancel, ignore result
+            return
+
+        box = self._mailboxes[mailbox_id]
+        box.deposit_result(result)
+
+        if box.has_task_waiting:
+            assert box.dest_addr is not None
+            task = self._tasks[box.dest_addr]
+
+            if task.wake_on_next or box.ready:
+                self._ready_task_ids.put(box.dest_addr)  # Wake it
+
+    def _handle_cancel(self, addr: RuntimeAddress) -> None:
+        """
+        Remove `addr` and its children tasks from this worker.
+
+        Notes:
+            Since `self._ready_task_ids' is a queue, it is more efficient
+            to discard cancelled tasks when popping from it. Therefore, we
+            do not do anything with `self._ready_task_ids` here.
+
+            Also, we also don't need to send out cancel messages for
+            cancelled children tasks since other workers can evaluate that
+            for themselves using breadcrumbs and the original `addr` cancel
+            message.
+        """
+        self._cancelled_task_ids.add(addr)
+
+        # Remove all tasks that are children of `addr` from initialized tasks
+        for key, task in self._tasks.items():
+            if task.is_descendant_of(addr):
+                for mailbox_id in self._tasks[key].owned_mailboxes:
+                    self._mailboxes.pop(mailbox_id)
+        self._tasks = {
+            a: t for a, t in self._tasks.items()
+            if not t.is_descendant_of(addr)
+        }
+
+        # Remove all tasks that are children of `addr` from delayed tasks
+        self._delayed_tasks = [
+            t for t in self._delayed_tasks
+            if not t.is_descendant_of(addr)
+        ]
+
+    def _get_next_ready_task(self) -> RuntimeTask | None:
+        """Return the next ready task if one exists, otherwise None."""
+        while True:
+            if self._ready_task_ids.empty():
+                if len(self._delayed_tasks) > 0:
+                    self._add_task(self._delayed_tasks.pop())
+                    continue
+                return None
+
+            addr = self._ready_task_ids.get()
+
+            if addr in self._cancelled_task_ids or addr not in self._tasks:
+                # When a task is cancelled on the worker it is not removed
+                # from the ready queue because it is much cheaper to just
+                # discard cancelled tasks as they come out.
+                continue
+
+            task = self._tasks[addr]
+
+            if any(bcb in self._cancelled_task_ids for bcb in task.breadcrumbs):
+                # If any of the selected tasks ancestor tasks are cancelled
+                # then discard this one too. Each breadcrumb (bcb) is a
+                # task address (unique system-wide task id) of an ancestor
+                # task.
+                continue
+
+            return task
+
+    def _try_step_next_ready_task(self) -> None:
+        """Select a task to run, and advance it one step."""
+        task = self._get_next_ready_task()
+
+        if task is None:
+            # Nothing to do
+            return
+
+        try:
+            self._active_task = task
+
+            # Perform a step of the task and get the future it awaits on
+            future = task.step(self._get_desired_result(task))
+
+            self._process_await(task, future)
+
+        except StopIteration as e:
+            self._process_task_completion(task, e.value)
+
+        except Exception:
+            assert self._active_task is not None  # for type checker
+
+            # Bubble up errors
+            exc_info = sys.exc_info()
+            error_str = ''.join(traceback.format_exception(*exc_info))
+            error_payload = (self._active_task.comp_task_id, error_str)
+            self._outgoing.append((RuntimeMessage.ERROR, error_payload))
+
+        finally:
+            self._active_task = None
+
+    def _process_await(self, task: RuntimeTask, future: RuntimeFuture) -> None:
+        """Process a task's await request."""
+        if not isinstance(future, RuntimeFuture):
+            raise RuntimeError('Can only await on a BQSKit RuntimeFuture.')
+
+        if future.mailbox_id not in self._mailboxes:
+            raise RuntimeError('Cannot await on a canceled task.')
+
+        box = self._mailboxes[future.mailbox_id]
+
+        # Let the mailbox know this task is waiting
+        box.dest_addr = task.return_address
+        task.desired_box_id = future.mailbox_id
+
+        if future._next_flag:
+            # Set from Worker.next, implies the task wants the next result
+            if box.ready:
+                m = 'Cannot wait for next results on a complete task.'
+                raise RuntimeError(m)
+            task.wake_on_next = True
+
+        elif box.ready:
+            self._ready_task_ids.put(task.return_address)
+
+    def _process_task_completion(self, task: RuntimeTask, result: Any) -> None:
+        """Package and send out task result."""
+        assert task is self._active_task
+        packaged_result = RuntimeResult(task.return_address, result, self._id)
+
+        if task.return_address.worker_id == self._id:
+            self._handle_result(packaged_result)
+            self._outgoing.append((RuntimeMessage.UPDATE, -1))
+            # Let manager know this worker has one less task
+            # without sending a result
+        else:
+            self._outgoing.append((RuntimeMessage.RESULT, packaged_result))
+
+        # Remove task
+        self._tasks.pop(task.return_address)
+
+        # Cancel any open tasks
+        for mailbox_id in self._active_task.owned_mailboxes:
+            # If task is complete, simply discard result
+            if mailbox_id in self._mailboxes:
+                if self._mailboxes[mailbox_id].ready:
+                    self._mailboxes.pop(mailbox_id)
+                    continue
+
+            # Otherwise send a cancel message
+            self.cancel(RuntimeFuture(mailbox_id))
+
+        # Start delayed task
+        if self._ready_task_ids.empty() and len(self._delayed_tasks) > 0:
+            self._add_task(self._delayed_tasks.pop())
+
+    def _get_desired_result(self, task: RuntimeTask) -> Any:
+        """Retrieve the task's desired result from the mailboxes."""
+        if task.desired_box_id is None:
+            return None
+
+        box = self._mailboxes[task.desired_box_id]
+
+        if task.wake_on_next:
+            fresh_results = box.get_new_results()
+            assert len(fresh_results) > 0
+            return fresh_results
+
+        assert box.ready
+        task.owned_mailboxes.remove(task.desired_box_id)
+        return self._mailboxes.pop(task.desired_box_id).result
+
+    def _get_new_mailbox_id(self) -> int:
+        """Return a new unique mailbox id."""
+        new_id = self._mailbox_counter
+        self._mailbox_counter += 1
+        return new_id
+
+    def submit(
+        self,
+        fn: Callable[..., Any],
+        *args: Any,
+        **kwargs: Any,
+    ) -> RuntimeFuture:
+        """Submit `fn` as a task to the runtime."""
+        assert self._active_task is not None
+        # Group fnargs together
+        fnarg = (fn, args, kwargs)
+
+        # Create a new mailbox
+        mailbox_id = self._get_new_mailbox_id()
+        self._mailboxes[mailbox_id] = WorkerMailbox.new_mailbox()
+        self._active_task.owned_mailboxes.append(mailbox_id)
+
+        # Create the task
+        task = RuntimeTask(
+            fnarg,
+            RuntimeAddress(self._id, mailbox_id, 0),
+            self._active_task.comp_task_id,
+            self._active_task.breadcrumbs + (self._active_task.return_address,),
+            self._active_task.logging_level,
+            self._active_task.max_logging_depth,
+        )
+
+        # Submit the task (on the next cycle)
+        self._outgoing.append((RuntimeMessage.SUBMIT, task))
+
+        # Return future pointing to the mailbox
+        return RuntimeFuture(mailbox_id)
+
+    def map(
+        self,
+        fn: Callable[..., Any],
+        *args: Any,
+        **kwargs: Any,
+    ) -> RuntimeFuture:
+        """Map `fn` over the input arguments distributed across the runtime."""
+        assert self._active_task is not None
+        # Group fnargs together
+        fnargs = []
+        if len(args) == 1:
+            for arg in args[0]:
+                fnargs.append((fn, (arg,), kwargs))
+
+        else:
+            for subargs in zip(*args):
+                fnargs.append((fn, subargs, kwargs))
+
+        if len(fnargs) == 0:
+            raise RuntimeError('Unable to map 0 tasks.')
+
+        # Create a new mailbox
+        mailbox_id = self._get_new_mailbox_id()
+        self._mailboxes[mailbox_id] = WorkerMailbox.new_mailbox(len(fnargs))
+        self._active_task.owned_mailboxes.append(mailbox_id)
+
+        # Create the tasks
+        breadcrumbs = self._active_task.breadcrumbs
+        breadcrumbs += (self._active_task.return_address,)
+        tasks = [
+            RuntimeTask(
+                fnarg,
+                RuntimeAddress(self._id, mailbox_id, i),
+                self._active_task.comp_task_id,
+                breadcrumbs,
+                self._active_task.logging_level,
+                self._active_task.max_logging_depth,
+            )
+            for i, fnarg in enumerate(fnargs)
+        ]
+
+        # Submit the tasks
+        self._outgoing.append((RuntimeMessage.SUBMIT_BATCH, tasks))
+
+        # Return future pointing to the mailbox
+        return RuntimeFuture(mailbox_id)
+
+    def cancel(self, future: RuntimeFuture) -> None:
+        """Cancel all tasks associated with `future`."""
+        assert self._active_task is not None
+        num_slots = self._mailboxes[future.mailbox_id].expected_num_results
+        self._active_task.owned_mailboxes.remove(future.mailbox_id)
+        self._mailboxes.pop(future.mailbox_id)
+        addrs = [
+            RuntimeAddress(self._id, future.mailbox_id, slot_id)
+            for slot_id in range(num_slots)
+        ]
+        msgs = [(RuntimeMessage.CANCEL, addr) for addr in addrs]
+        self._outgoing.extend(msgs)
+
+    async def next(self, future: RuntimeFuture) -> list[tuple[int, Any]]:
+        """
+        Wait for and return the next batch of results from a map task.
+
+        Returns:
+            (list[tuple[int, Any]]): A list of the results that arrived
+                since the last time this was called. On the first call,
+                all results that have arrived since the task started are
+                returned. Each result is paired with the index of its
+                arguments in the original map call.
+        """
+        if future._done:
+            raise RuntimeError('Cannot wait on an already completed result.')
+
+        future._next_flag = True
+        next_result_batch = await future
+        future._next_flag = False
+        return next_result_batch
+
+
+# Global variable containing reference to this process's worker object.
+_worker = None
+
+
+def start_worker(id: int, port: int) -> None:
+    """Start this process's worker."""
+    # Ignore interrupt signals on workers, boss will handle it for us
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+    # Purge all standard python logging configurations
+    for _, logger in logging.Logger.manager.loggerDict.items():
+        if isinstance(logger, logging.PlaceHolder):
+            continue
+        logger.handlers.clear()
+    logging.Logger.manager.loggerDict = {}
+
+    max_retries = 7
+    wait_time = .1
+    conn: Connection | None = None
+    family = 'AF_INET' if sys.platform == 'win32' else None
+    for _ in range(max_retries):
+        try:
+            conn = Client(('localhost', port), family)
+        except (ConnectionRefusedError, TimeoutError):
+            time.sleep(wait_time)
+            wait_time *= 2
+        else:
+            break
+
+    if conn is None:
+        raise RuntimeError('Unable to establish connection with manager.')
+
+    global _worker
+    _worker = Worker(id, conn)
+    _worker._loop()
+
+
+def get_worker() -> Worker:
+    """Return a handle on this process's worker."""
+    if _worker is None:
+        raise RuntimeError('Worker has not been started.')
+    return _worker
diff --git a/bqskit/utils/math.py b/bqskit/utils/math.py
index 00f85d7af..63dbc9c55 100644
--- a/bqskit/utils/math.py
+++ b/bqskit/utils/math.py
@@ -38,7 +38,8 @@ def dexpmv(
         (2008): 17-24.
     """
 
-    e = np.log2(np.linalg.norm(M, np.inf))
+    norm = np.linalg.norm(M, np.inf)
+    e = np.log2(norm) if norm != 0 else -np.inf
     r = int(max(0, e + 1))
     M = M / (2 ** r)
     dM = dM / (2 ** r)
diff --git a/bqskit/utils/typing.py b/bqskit/utils/typing.py
index 4e2f935ce..17cf00847 100644
--- a/bqskit/utils/typing.py
+++ b/bqskit/utils/typing.py
@@ -142,11 +142,11 @@ def is_matrix(M: np.typing.ArrayLike) -> bool:
         M = np.array(M)
 
     if len(M.shape) != 2:
-        _logger.debug('M is not an 2-dimensional array.')
+        # _logger.debug('M is not an 2-dimensional array.')
         return False
 
     if M.dtype.kind not in 'biufc':
-        _logger.debug('M is not a numeric array.')
+        # _logger.debug('M is not a numeric array.')
         return False
 
     return True
diff --git a/docs/conf.py b/docs/conf.py
index f9ab6857d..dd30c06b2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -93,13 +93,10 @@
 myst_enable_extensions = ['dollarmath', 'amsmath']
 autodoc_mock_imports = [
     'bqskitrs',
-    'threadpoolctl',
-    'dask',
     'scipy',
     'numpy',
     'qiskit',
     'lark-parser',
-    'distributed',
     'hypothesis',
     'pytket',
     'cirq',
diff --git a/docs/index.rst b/docs/index.rst
index 8ca903aa5..bb7d6a096 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -31,3 +31,4 @@ our `tutorial series. <https://github.com/BQSKit/bqskit-tutorial/>`_
    source/qis
    source/utils
    source/exec
+   source/runtime
diff --git a/docs/source/runtime.rst b/docs/source/runtime.rst
new file mode 100644
index 000000000..5a87280d3
--- /dev/null
+++ b/docs/source/runtime.rst
@@ -0,0 +1,4 @@
+.. automodule:: bqskit.runtime
+   :no-members:
+   :no-inherited-members:
+   :no-special-members:
diff --git a/examples/qfast.py b/examples/qfast.py
index 8124c4bf7..72c1911b5 100644
--- a/examples/qfast.py
+++ b/examples/qfast.py
@@ -1,7 +1,6 @@
 """This script is contains a simple use case of the QFAST synthesis method."""
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir import Circuit
 from bqskit.passes import ForEachBlockPass
@@ -15,16 +14,17 @@
 # circuit.
 circuit = Circuit.from_unitary(UnitaryMatrix.random(3))
 
-# We will now define the CompilationTask we want to run.
-task = CompilationTask(
-    circuit, [
-        QFASTDecompositionPass(),
-        ForEachBlockPass([LEAPSynthesisPass(), ScanningGateRemovalPass()]),
-        UnfoldPass(),
-    ],
-)
+# We now define our synthesis workflow utilizing the QFAST algorithm.
+workflow = [
+    QFASTDecompositionPass(),
+    ForEachBlockPass([
+        LEAPSynthesisPass(),  # LEAP performs native gate instantiation
+        ScanningGateRemovalPass(),  # Gate removal optimizing gate counts
+    ]),
+    UnfoldPass(),
+]
 
 # Finally let's create create the compiler and execute the CompilationTask.
 with Compiler() as compiler:
-    compiled_circuit = compiler.compile(task)
+    compiled_circuit = compiler.compile(circuit, workflow)
     print(compiled_circuit.gate_counts)
diff --git a/examples/quest.py b/examples/quest.py
index 8ef008043..8115fa82c 100644
--- a/examples/quest.py
+++ b/examples/quest.py
@@ -3,7 +3,6 @@
 
 import logging
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.exec.runners.quest import QuestRunner
 from bqskit.exec.runners.sim import SimulationRunner
@@ -23,18 +22,15 @@
     # circuit.
     circuit = Circuit.from_unitary(UnitaryMatrix.random(4))
 
-    # We will now define the CompilationTask we want to run.
-    task = CompilationTask(
-        circuit, [
-            QFASTDecompositionPass(),
-            ForEachBlockPass([LEAPSynthesisPass(), ScanningGateRemovalPass()]),
-            UnfoldPass(),
-        ],
-    )
+    workflow = [
+        QFASTDecompositionPass(),
+        ForEachBlockPass([LEAPSynthesisPass(), ScanningGateRemovalPass()]),
+        UnfoldPass(),
+    ]
 
     # Finally let's create create the compiler and execute the CompilationTask.
     with Compiler() as compiler:
-        compiled_circuit = compiler.compile(task)
+        compiled_circuit = compiler.compile(circuit, workflow)
 
         # Execute the circuit with Quest
         quest_runner = QuestRunner(SimulationRunner(), compiler=compiler)
diff --git a/examples/tutorials/Partitioning and Optimization.ipynb b/examples/tutorials/Partitioning and Optimization.ipynb
index 4c6f72743..b7b16e40b 100644
--- a/examples/tutorials/Partitioning and Optimization.ipynb
+++ b/examples/tutorials/Partitioning and Optimization.ipynb
@@ -39,11 +39,12 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "563b666f",
    "metadata": {},
    "source": [
-    "We will partition the circuit and then use the `ForEachBlockPass` to perform operations on the individual blocks. Note the `ForEachBlockPass` will run the sub tasks in parallel using dask."
+    "We will partition the circuit and then use the `ForEachBlockPass` to perform operations on the individual blocks. Note the `ForEachBlockPass` will run the sub tasks in parallel."
    ]
   },
   {
diff --git a/setup.cfg b/setup.cfg
index b3fac1c16..3a97bce05 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = bqskit
-version = 1.0.4
+version = 1.1.0a1
 description = Berkeley Quantum Synthesis Toolkit
 long_description = file: README.md
 long_description_content_type = text/markdown
@@ -38,29 +38,31 @@ project_urls =
 [options]
 packages = find:
 install_requires =
-    bqskitrs>=0.3.0
-    dask[distributed]>=2022.07.01,<=2022.12.1
+    bqskitrs>=0.4.0rc3
     lark-parser
     numpy>=1.22.0
     scipy>=1.8.0
-    threadpoolctl
     typing-extensions>=4.0.0
 python_requires = >=3.8, <4
 
 [options.packages.find]
 exclude =
-    tests
+    tests*
     examples
 
+[options.entry_points]
+console_scripts =
+    bqskit-server = bqskit.runtime.detached:start_server
+    bqskit-manager = bqskit.runtime.manager:start_manager
+
 [options.extras_require]
 dev =
-    hypothesis
+    hypothesis[zoneinfo]
     mypy
     pre-commit
+    psutil
     pytest
-    pytket-qiskit
     tox
-    tzdata
 ext =
     cirq>=1.0.0
     pytket>=1.5.0


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 04aa57b7a..3e2c1ab59 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,6 +8,7 @@ on:
 jobs:
   tests:
     strategy:
+      fail-fast: false
       matrix:
         python-version: ['3.8', '3.9', '3.10', '3.11']
         os: [macOS-latest, windows-latest, ubuntu-latest]
@@ -20,10 +21,12 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Upgrade python environment
       run: python -m pip install --upgrade virtualenv setuptools pip
+    - name: Upgrade test dependencies
+      run: python -m pip install psutil pytest 'hypothesis[zoneinfo]' qiskit pytket qutip cirq
     - name: Install BQSKit
       env:
         SYSTEM_VERSION_COMPAT: 0
         NUMBER_RANDOM_CIRCUITS: 100
-      run: pip install '.[ext,dev]'
+      run: pip install .
     - name: Run tests
       run: pytest
diff --git a/tests/bqskitrs/__init__.py b/tests/bqskitrs/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/compiler/__init__.py b/tests/compiler/__init__.py
index e69de29bb..c96f8be82 100644
--- a/tests/compiler/__init__.py
+++ b/tests/compiler/__init__.py
@@ -0,0 +1,2 @@
+# TODO: Test compiler.submit with no circuit/comptask sent
+from __future__ import annotations
diff --git a/tests/compiler/compile/__init__.py b/tests/compiler/compile/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/compiler/compile/test_prep.py b/tests/compiler/compile/test_prep.py
new file mode 100644
index 000000000..b685efbc1
--- /dev/null
+++ b/tests/compiler/compile/test_prep.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+from bqskit import compile
+from bqskit.ir import Circuit
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.opt.cost import HilbertSchmidtCostGenerator
+from bqskit.ir.opt.minimizers import LBFGSMinimizer
+from bqskit.qis.state import StateVector
+from bqskit.qis.state.system import StateSystem
+from bqskit.qis.unitary import UnitaryMatrix
+
+
+def test_state_prep() -> None:
+    state = StateVector.random(3)
+    out_circuit = compile(state)
+    out_state = out_circuit.get_statevector(StateVector.zero(3))
+    assert out_state.get_distance_from(state) < 1e-8
+
+
+def test_state_inst_2() -> None:
+    state = StateVector.random(2)
+    circuit = Circuit(2)
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.instantiate(
+        state,
+        minimizer=LBFGSMinimizer(),
+        cost_fn_gen=HilbertSchmidtCostGenerator(),
+    )
+    out_state = circuit.get_statevector(StateVector.zero(2))
+    assert out_state.get_distance_from(state) < 1e-8
+
+
+def test_state_inst_3() -> None:
+    state = StateVector.random(3)
+    circuit = Circuit(3)
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.append_gate(CXGate(), (0, 1))
+    circuit.append_gate(CXGate(), (1, 2))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(U3Gate(), 2)
+    circuit.instantiate(
+        state,
+        minimizer=LBFGSMinimizer(),
+        cost_fn_gen=HilbertSchmidtCostGenerator(),
+    )
+    out_state = circuit.get_statevector(StateVector.zero(3))
+    assert out_state.get_distance_from(state) < 1e-8
+
+
+def test_state_map() -> None:
+    num_qudits = 3
+    utry = UnitaryMatrix.random(num_qudits)
+    state_map = {}
+    for i in range(2):
+        in_state = StateVector.random(num_qudits)
+        out_state = utry.get_statevector(in_state)
+        state_map[in_state] = out_state
+    system = StateSystem(state_map)
+    out_circuit = compile(system)
+    print(out_circuit.gate_counts)
+
+    for in_state, out_state in state_map.items():
+        test_out_state = out_circuit.get_statevector(in_state)
+        assert out_state.get_distance_from(test_out_state) < 1e-5
diff --git a/tests/compiler/compile/test_synthesis.py b/tests/compiler/compile/test_synthesis.py
index 0cc031b25..dec43ec03 100644
--- a/tests/compiler/compile/test_synthesis.py
+++ b/tests/compiler/compile/test_synthesis.py
@@ -4,8 +4,7 @@
 
 from bqskit import compile
 from bqskit import MachineModel
-from bqskit.compiler.compiler import Compiler
-from bqskit.compiler.machine import default_gate_set
+from bqskit.compiler.machine import default_qubit_gate_set
 from bqskit.ext.cirq.models import google_gate_set
 from bqskit.ext.honeywell import honeywell_gate_set
 from bqskit.ext.rigetti import rigetti_gate_set
@@ -26,7 +25,7 @@
         {U3Gate()},
         {PhasedXZGate()},
         {U1qGate(), XGate()},
-        default_gate_set,
+        default_qubit_gate_set,
         rigetti_gate_set,
         honeywell_gate_set,
         google_gate_set,
@@ -38,25 +37,23 @@ def test_single_qudit_synthesis(
     sq_utry: UnitaryMatrix,
     optimization_level: int,
     gate_set: set[Gate],
-    compiler: Compiler,
 ) -> None:
     out_circuit = compile(
         sq_utry,
         model=MachineModel(1, gate_set=gate_set),
         optimization_level=optimization_level,
-        compiler=compiler,
     )
     if U3Gate() in gate_set:
         assert out_circuit.num_operations == 1
     assert out_circuit.num_qudits == 1
     assert len(out_circuit.gate_set.difference(gate_set)) == 0
-    assert out_circuit.get_unitary().get_distance_from(sq_utry, 1) < 1e-10
+    assert out_circuit.get_unitary().get_distance_from(sq_utry, 1) < 1e-8
 
 
 @pytest.mark.parametrize('tq_utry', [UnitaryMatrix.random(2) for i in range(5)])
 @pytest.mark.parametrize(
     'gate_set', [
-        default_gate_set,
+        default_qubit_gate_set,
         rigetti_gate_set,
         honeywell_gate_set,
         google_gate_set,
@@ -67,22 +64,20 @@ def test_two_qudit_synthesis(
     tq_utry: UnitaryMatrix,
     optimization_level: int,
     gate_set: set[Gate],
-    compiler: Compiler,
 ) -> None:
     out_circuit = compile(
         tq_utry,
         model=MachineModel(2, gate_set=gate_set),
         optimization_level=optimization_level,
-        compiler=compiler,
     )
     assert out_circuit.num_qudits == 2
     assert len(out_circuit.gate_set.difference(gate_set)) == 0
-    assert out_circuit.get_unitary().get_distance_from(tq_utry, 1) < 1e-10
+    assert out_circuit.get_unitary().get_distance_from(tq_utry, 1) < 1e-8
 
 
 @pytest.mark.parametrize(
     'gate_set', [
-        default_gate_set,
+        default_qubit_gate_set,
         rigetti_gate_set,
         honeywell_gate_set,
         {IToffoliGate(), U3Gate()},
@@ -93,39 +88,49 @@ def test_three_qudit_synthesis(
     toffoli_unitary: UnitaryMatrix,
     optimization_level: int,
     gate_set: set[Gate],
-    compiler: Compiler,
 ) -> None:
     out_circuit = compile(
         toffoli_unitary,
         model=MachineModel(3, gate_set=gate_set),
         optimization_level=optimization_level,
-        compiler=compiler,
     )
     assert out_circuit.num_qudits == 3
     assert len(out_circuit.gate_set.difference(gate_set)) == 0
     utry = toffoli_unitary
-    assert out_circuit.get_unitary().get_distance_from(utry, 1) < 1e-10
+    assert out_circuit.get_unitary().get_distance_from(utry, 1) < 1e-8
 
 
-def test_fail_on_larger_max_synthesis_size(compiler: Compiler) -> None:
+def test_fail_on_larger_max_synthesis_size() -> None:
     utry = UnitaryMatrix.random(4)
     with pytest.raises(ValueError):
-        compile(utry, max_synthesis_size=3, compiler=compiler)
+        compile(utry, max_synthesis_size=3)
 
 
 @pytest.mark.parametrize('dim', [2, 4, 8])
 def test_identity_synthesis(
     optimization_level: int,
     dim: int,
-    compiler: Compiler,
 ) -> None:
     out_circuit = compile(
         UnitaryMatrix.identity(dim),
         optimization_level=optimization_level,
-        compiler=compiler,
     )
     assert out_circuit.get_unitary().get_distance_from(
         UnitaryMatrix.identity(dim), 1,
-    ) < 1e-10
+    ) < 1e-8
     if optimization_level == 3:
         assert out_circuit.num_operations <= 3
+
+
+# @pytest.mark.parametrize('qudits', [1, 2])
+# def test_qutrit_synthesis(optimization_level: int, qudits: int) -> None:
+#     in_utry = UnitaryMatrix.random(qudits, [3] * qudits)
+#     out_circuit = compile(in_utry, optimization_level=optimization_level)
+#     assert out_circuit.get_unitary().get_distance_from(in_utry, 1) < 1e-8
+
+
+# @pytest.mark.parametrize('qudits', [1, 2])
+# def test_ququart_synthesis(optimization_level: int, qudits: int) -> None:
+#     in_utry = UnitaryMatrix.random(qudits, [4] * qudits)
+#     out_circuit = compile(in_utry, optimization_level=optimization_level)
+#     assert out_circuit.get_unitary().get_distance_from(in_utry, 1) < 1e-8
diff --git a/tests/compiler/test_compiler.py b/tests/compiler/test_compiler.py
index da0220fa8..1087a1905 100644
--- a/tests/compiler/test_compiler.py
+++ b/tests/compiler/test_compiler.py
@@ -2,37 +2,55 @@
 from __future__ import annotations
 
 import logging
+import traceback
 from io import StringIO
-from typing import Any
-from unittest.mock import patch
 
 import pytest
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.compiler import Compiler
-from bqskit.compiler.task import CompilationTask
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 
 
-def test_errors_raised_locally(compiler: Compiler) -> None:
-    class ErrorPass(BasePass):
-        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-            raise RuntimeError()
+class ErrorPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        raise RuntimeError('TestError')
 
-    task = CompilationTask(Circuit(1), [ErrorPass()])
 
-    with pytest.raises(RuntimeError):
-        compiler.compile(task)
+class LogPass(BasePass):
+    async def run(
+        self,
+        circuit: Circuit,
+        data: PassData,
+    ) -> None:
+        logging.getLogger('bqskit.dummy').debug('Test.')
 
 
-def test_log_msg_printed_locally(compiler: Compiler) -> None:
-    class LogPass(BasePass):
-        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-            logging.getLogger('bqskit.dummy').debug('Test.')
+def test_errors_raised_locally() -> None:
+    with pytest.raises(RuntimeError) as exc_info:
+        with Compiler() as compiler:
+            compiler.compile(Circuit(1), [ErrorPass()])
 
-    task = CompilationTask(Circuit(1), [LogPass()])
-    logging.getLogger('bqskit').setLevel(logging.DEBUG)
-    with patch('sys.stdout', new_callable=StringIO) as mock_out:
-        compiler.compile(task)
-        print(mock_out.getvalue())
-    logging.getLogger('bqskit').setLevel(logging.WARNING)
+    error_str = ''.join(traceback.format_exception(*exc_info._excinfo))
+    assert 'TestError' in error_str
+
+
+def test_simple_creation() -> None:
+    com = Compiler(num_workers=6)
+    com.close()
+
+    assert True
+
+
+def test_log_msg_printed_locally() -> None:
+    logger = logging.getLogger('bqskit')
+    logger.setLevel(logging.DEBUG)
+    handler = logging.StreamHandler(StringIO())
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    with Compiler() as compiler:
+        compiler.compile(Circuit(1), [LogPass()])
+    assert 'Test.' in handler.stream.getvalue()
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
diff --git a/tests/compiler/test_machine.py b/tests/compiler/test_machine.py
index 586b7e40e..8c6e4702f 100644
--- a/tests/compiler/test_machine.py
+++ b/tests/compiler/test_machine.py
@@ -1,3 +1,4 @@
+# TODO: is_compatabile
 from __future__ import annotations
 
 import pytest
diff --git a/tests/conftest.py b/tests/conftest.py
index 812bb6a48..1d6c97706 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,7 +19,7 @@
 from hypothesis import settings
 from scipy.stats import unitary_group
 
-from bqskit.compiler.compiler import Compiler
+from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import CNOTGate
@@ -140,6 +140,13 @@ def compiler() -> Iterator[Compiler]:
     compiler.close()
 
 
+@pytest.fixture
+def fresh_compiler() -> Iterator[Compiler]:
+    compiler = Compiler()
+    yield compiler
+    compiler.close()
+
+
 @pytest.fixture
 def toffoli_unitary() -> UnitaryMatrix:
     return UnitaryMatrix(TOFFOLI)
diff --git a/tests/exec/__init__.py b/tests/exec/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/ext/test_cirq.py b/tests/ext/test_cirq.py
index f3eff355e..f68bb557b 100644
--- a/tests/ext/test_cirq.py
+++ b/tests/ext/test_cirq.py
@@ -4,17 +4,16 @@
 import pytest
 pytest.importorskip('cirq')
 
-import cirq
-from cirq import Circuit as QuantumCircuit
-
-from bqskit.compiler.compile import compile
-from bqskit.compiler.compiler import Compiler
-from bqskit.ext import bqskit_to_cirq
-from bqskit.ext import cirq_to_bqskit
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates import HGate
 from bqskit.qis import UnitaryMatrix
+from bqskit.ir.gates import HGate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.circuit import Circuit
+from bqskit.ext import cirq_to_bqskit
+from bqskit.ext import bqskit_to_cirq
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.compile import compile
+from cirq import Circuit as QuantumCircuit
+import cirq
 
 
 class TestTranslate:
diff --git a/tests/ext/test_pytket.py b/tests/ext/test_pytket.py
index 2fc388935..1be35559d 100644
--- a/tests/ext/test_pytket.py
+++ b/tests/ext/test_pytket.py
@@ -3,20 +3,20 @@
 
 import pytest
 pytest.importorskip('pytket')
+pytest.importorskip('pytket.extensions.qiskit')
 
-import numpy as np
-from pytket import OpType
-from pytket.circuit import Circuit as QubitCircuit
-from pytket.extensions.qiskit import AerUnitaryBackend
-
-from bqskit.compiler.compile import compile
-from bqskit.compiler.compiler import Compiler
-from bqskit.ext import bqskit_to_pytket
-from bqskit.ext import pytket_to_bqskit
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates import U3Gate
 from bqskit.qis import UnitaryMatrix
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.circuit import Circuit
+from bqskit.ext import pytket_to_bqskit
+from bqskit.ext import bqskit_to_pytket
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.compile import compile
+from pytket.extensions.qiskit import AerUnitaryBackend
+from pytket.circuit import Circuit as QubitCircuit
+from pytket import OpType
+import numpy as np
 
 
 class TestTranslate:
@@ -67,10 +67,7 @@ def pytket_circuit(self) -> QubitCircuit:
         return circuit
 
     def get_unitary(self, qc: QubitCircuit) -> np.ndarray:  # type: ignore
-        backend = AerUnitaryBackend()
-        handle = backend.process_circuit(qc)
-        result = backend.get_result(handle)
-        return result.get_unitary()
+        return qc.get_unitary()
 
     def test_bqskit_to_bqskit(self, bqskit_circuit: Circuit) -> None:
         in_utry = bqskit_circuit.get_unitary()
@@ -84,28 +81,18 @@ def test_pytket_to_pytket(self, pytket_circuit: QubitCircuit) -> None:
         out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
         assert in_utry.get_distance_from(out_utry) < 1e-7
 
-    def test_compile_bqskit(
-        self,
-        pytket_circuit: QubitCircuit,
-        compiler: Compiler,
-    ) -> None:
+    def test_compile_bqskit(self, pytket_circuit: QubitCircuit) -> None:
         in_utry = UnitaryMatrix(self.get_unitary(pytket_circuit))
         bqskit_circuit = pytket_to_bqskit(pytket_circuit)
-        bqskit_out_circuit = compile(
-            bqskit_circuit, max_synthesis_size=2, compiler=compiler,
-        )
+        bqskit_out_circuit = compile(bqskit_circuit, max_synthesis_size=2)
         out_circuit = bqskit_to_pytket(bqskit_out_circuit)
         out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
         assert in_utry.get_distance_from(out_utry) < 1e-5
 
-    def test_synthesis_bqskit(
-        self,
-        pytket_circuit: QubitCircuit,
-        compiler: Compiler,
-    ) -> None:
+    def test_synthesis_bqskit(self, pytket_circuit: QubitCircuit) -> None:
         in_utry = UnitaryMatrix(self.get_unitary(pytket_circuit))
         bqskit_circuit = pytket_to_bqskit(pytket_circuit)
-        bqskit_out_circuit = compile(bqskit_circuit, compiler=compiler)
+        bqskit_out_circuit = compile(bqskit_circuit)
         out_circuit = bqskit_to_pytket(bqskit_out_circuit)
         out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
         assert in_utry.get_distance_from(out_utry) < 1e-5
diff --git a/tests/ext/test_qiskit.py b/tests/ext/test_qiskit.py
index e2151f2c5..b96949e09 100644
--- a/tests/ext/test_qiskit.py
+++ b/tests/ext/test_qiskit.py
@@ -4,18 +4,16 @@
 import pytest
 pytest.importorskip('qiskit')
 
-import qiskit.quantum_info as qi
-from qiskit import QuantumCircuit
-from qiskit import transpile
-
-from bqskit.compiler.compile import compile
-from bqskit.compiler.compiler import Compiler
-from bqskit.ext import bqskit_to_qiskit
-from bqskit.ext import qiskit_to_bqskit
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates import U3Gate
 from bqskit.qis import UnitaryMatrix
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.circuit import Circuit
+from bqskit.ext import qiskit_to_bqskit
+from bqskit.ext import bqskit_to_qiskit
+from bqskit.compiler.compile import compile
+from qiskit import transpile
+from qiskit import QuantumCircuit
+import qiskit.quantum_info as qi
 
 
 class TestTranslate:
@@ -52,7 +50,7 @@ def qiskit_circuit(self) -> QuantumCircuit:
         circuit.cnot(0, 1)
         circuit.cnot(0, 2)
         circuit.cnot(0, 2)
-        circuit.u3(1, 2.4, 3, 0)
+        circuit.u(1, 2.4, 3, 0)
         circuit.u(1, 2.2, 3, 1)
         circuit.u(1, 2.1, 3, 2)
         circuit.u(1, 2.1, 3, 2)
@@ -78,30 +76,20 @@ def test_qiskit_to_qiskit(self, qiskit_circuit: QuantumCircuit) -> None:
         out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
         assert in_utry.get_distance_from(out_utry) < 1e-7
 
-    def test_compile_bqskit(
-        self,
-        qiskit_circuit: QuantumCircuit,
-        compiler: Compiler,
-    ) -> None:
+    def test_compile_bqskit(self, qiskit_circuit: QuantumCircuit) -> None:
         qc = qiskit_circuit
         in_utry = UnitaryMatrix(qi.Operator(qc).data)
         bqskit_circuit = qiskit_to_bqskit(qc)
-        bqskit_out_circuit = compile(
-            bqskit_circuit, max_synthesis_size=2, compiler=compiler,
-        )
+        bqskit_out_circuit = compile(bqskit_circuit, max_synthesis_size=2)
         out_circuit = bqskit_to_qiskit(bqskit_out_circuit)
         out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
         assert in_utry.get_distance_from(out_utry) < 1e-5
 
-    def test_synthesis_bqskit(
-        self,
-        qiskit_circuit: QuantumCircuit,
-        compiler: Compiler,
-    ) -> None:
+    def test_synthesis_bqskit(self, qiskit_circuit: QuantumCircuit) -> None:
         qc = qiskit_circuit
         in_utry = UnitaryMatrix(qi.Operator(qc).data)
         bqskit_circuit = qiskit_to_bqskit(qc)
-        bqskit_out_circuit = compile(bqskit_circuit, compiler=compiler)
+        bqskit_out_circuit = compile(bqskit_circuit)
         out_circuit = bqskit_to_qiskit(bqskit_out_circuit)
         out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
         assert in_utry.get_distance_from(out_utry) < 1e-5
diff --git a/tests/ext/test_qutip.py b/tests/ext/test_qutip.py
index 78204a3c4..5851a28e5 100644
--- a/tests/ext/test_qutip.py
+++ b/tests/ext/test_qutip.py
@@ -4,17 +4,16 @@
 import pytest
 pytest.importorskip('qutip')
 
-from qutip import CircuitSimulator
-from qutip import QubitCircuit
-
-from bqskit.compiler.compile import compile
-from bqskit.compiler.compiler import Compiler
-from bqskit.ext import bqskit_to_qutip
-from bqskit.ext import qutip_to_bqskit
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates import U3Gate
 from bqskit.qis import UnitaryMatrix
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.circuit import Circuit
+from bqskit.ext import qutip_to_bqskit
+from bqskit.ext import bqskit_to_qutip
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.compile import compile
+from qutip.qip.circuit import QubitCircuit
+from qutip.qip.circuit import CircuitSimulator
 
 
 class TestTranslate:
diff --git a/tests/ir/__init__.py b/tests/ir/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/ir/circuit/test_batch_replace.py b/tests/ir/circuit/test_batch_replace.py
index 501621d56..62c1c1ea6 100644
--- a/tests/ir/circuit/test_batch_replace.py
+++ b/tests/ir/circuit/test_batch_replace.py
@@ -118,7 +118,7 @@ def test_random_batch_replace(self) -> None:
                 b = (b + 1) % num_q
             circ.append_gate(CNOTGate(), [a, b])
 
-        ScanPartitioner(2).run(circ, {})
+        circ.perform(ScanPartitioner(2))
 
         points = []
         ops = []
diff --git a/tests/ir/lang/__init__.py b/tests/ir/lang/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/control/__init__.py b/tests/passes/control/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/control/predicates/__init__.py b/tests/passes/control/predicates/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/control/predicates/test_andpredicate.py b/tests/passes/control/predicates/test_andpredicate.py
new file mode 100644
index 000000000..96bedaf53
--- /dev/null
+++ b/tests/passes/control/predicates/test_andpredicate.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+from bqskit.passes.control.predicates import AndPredicate
+
+
+class TruePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return True
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+def test_true_and_true() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = AndPredicate(TruePredicate(), TruePredicate())
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_true_and_false() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = AndPredicate(TruePredicate(), FalsePredicate())
+    assert not pred.get_truth_value(circuit, data)
+
+
+def test_false_and_true() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = AndPredicate(FalsePredicate(), TruePredicate())
+    assert not pred.get_truth_value(circuit, data)
+
+
+def test_false_and_false() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = AndPredicate(FalsePredicate(), FalsePredicate())
+    assert not pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_change.py b/tests/passes/control/predicates/test_change.py
new file mode 100644
index 000000000..dc2d939bc
--- /dev/null
+++ b/tests/passes/control/predicates/test_change.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import HGate
+from bqskit.passes.control.predicates import ChangePredicate
+
+
+def test_change_single_gate() -> None:
+    pred = ChangePredicate()
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    assert pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        assert not pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        circuit.append_gate(CXGate(), (0, 1))
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+
+def test_change_two_gate() -> None:
+    pred = ChangePredicate()
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    assert pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        assert not pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        circuit.append_gate(CXGate(), (0, 1))
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+        circuit.append_gate(HGate(), 0)
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+
+def test_change_two_gate_plus_minus() -> None:
+    pred = ChangePredicate()
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    circuit.append_gate(HGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+
+    assert pred.get_truth_value(circuit, data)
+
+    circuit.remove(CXGate())
+    circuit.append_gate(HGate(), 0)
+
+    assert pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_count.py b/tests/passes/control/predicates/test_count.py
new file mode 100644
index 000000000..cb4ca0298
--- /dev/null
+++ b/tests/passes/control/predicates/test_count.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import HGate
+from bqskit.passes.control.predicates import GateCountPredicate
+
+
+def test_count_single_gate() -> None:
+    pred = GateCountPredicate(CXGate())
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    assert pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        assert not pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        circuit.append_gate(CXGate(), (0, 1))
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+
+def test_count_two_gate() -> None:
+    pred = GateCountPredicate((CXGate(), HGate()))
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    assert pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        assert not pred.get_truth_value(circuit, data)
+
+    for _ in range(3):
+        circuit.append_gate(CXGate(), (0, 1))
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+        circuit.append_gate(HGate(), 0)
+
+        assert pred.get_truth_value(circuit, data)
+        for _ in range(3):
+            assert not pred.get_truth_value(circuit, data)
+
+
+def test_count_two_gate_plus_minus() -> None:
+    pred = GateCountPredicate((CXGate(), HGate()))
+    circuit = Circuit(2)
+    data = PassData(circuit)
+
+    circuit.append_gate(HGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+
+    assert pred.get_truth_value(circuit, data)
+
+    circuit.remove(CXGate())
+    circuit.append_gate(HGate(), 0)
+
+    assert not pred.get_truth_value(circuit, data)
+    # This is desired behavior
+    # If you want a true if either gate count has changed
+    # rather than use one GateCountPredicate, use an OrPredicate
+    # of two GateCountPredicates, or a ChangePredicate
diff --git a/tests/passes/control/predicates/test_multi.py b/tests/passes/control/predicates/test_multi.py
new file mode 100644
index 000000000..98dd21a01
--- /dev/null
+++ b/tests/passes/control/predicates/test_multi.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import CZGate
+from bqskit.ir.gates import XGate
+from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
+
+
+@pytest.mark.parametrize(
+    'gate_set', [
+        set(),
+        {CZGate()},
+        {CXGate()},
+        {CZGate(), CXGate()},
+    ],
+)
+def test_multi_physical(gate_set: set[Gate]) -> None:
+    pred = MultiPhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set=gate_set)
+    assert pred.get_truth_value(circuit, data) == (CXGate() in gate_set)
+
+
+def test_multi_physical_sq_not_in() -> None:
+    pred = MultiPhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={CXGate()})
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_multi_physical_mq_in() -> None:
+    pred = MultiPhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={XGate(), CXGate()})
+    assert pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_notpredicate.py b/tests/passes/control/predicates/test_notpredicate.py
new file mode 100644
index 000000000..0321da06c
--- /dev/null
+++ b/tests/passes/control/predicates/test_notpredicate.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+from bqskit.passes.control.predicates import NotPredicate
+
+
+class TruePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return True
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+def test_not_true() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    assert not NotPredicate(TruePredicate()).get_truth_value(circuit, data)
+
+
+def test_not_false() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    assert NotPredicate(FalsePredicate()).get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_orpredicate.py b/tests/passes/control/predicates/test_orpredicate.py
new file mode 100644
index 000000000..cfeecbf42
--- /dev/null
+++ b/tests/passes/control/predicates/test_orpredicate.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+from bqskit.passes.control.predicates import OrPredicate
+
+
+class TruePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return True
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+def test_true_and_true() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = OrPredicate(TruePredicate(), TruePredicate())
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_true_and_false() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = OrPredicate(TruePredicate(), FalsePredicate())
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_false_and_true() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = OrPredicate(FalsePredicate(), TruePredicate())
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_false_and_false() -> None:
+    circuit = Circuit(1)
+    data = PassData(circuit)
+    pred = OrPredicate(FalsePredicate(), FalsePredicate())
+    assert not pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_physical.py b/tests/passes/control/predicates/test_physical.py
new file mode 100644
index 000000000..31b61d4a9
--- /dev/null
+++ b/tests/passes/control/predicates/test_physical.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import HGate
+from bqskit.ir.gates import XGate
+from bqskit.passes.control.predicates import PhysicalPredicate
+
+
+@pytest.mark.parametrize(
+    'gate_set', [
+        set(),
+        {HGate()},
+        {XGate()},
+        {HGate(), XGate()},
+    ],
+)
+def test_physical(gate_set: set[Gate]) -> None:
+    pred = PhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set=gate_set)
+    assert pred.get_truth_value(circuit, data) == (XGate() in gate_set)
+
+
+def test_physical_mq_not_in() -> None:
+    pred = PhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={XGate()})
+    assert not pred.get_truth_value(circuit, data)
+
+
+def test_physical_mq_in() -> None:
+    pred = PhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={XGate(), CXGate()})
+    assert pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_singlephy.py b/tests/passes/control/predicates/test_singlephy.py
new file mode 100644
index 000000000..efbf4169c
--- /dev/null
+++ b/tests/passes/control/predicates/test_singlephy.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import CXGate
+from bqskit.ir.gates import HGate
+from bqskit.ir.gates import XGate
+from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
+
+
+@pytest.mark.parametrize(
+    'gate_set', [
+        set(),
+        {HGate()},
+        {XGate()},
+        {HGate(), XGate()},
+    ],
+)
+def test_single_physical(gate_set: set[Gate]) -> None:
+    pred = SinglePhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set=gate_set)
+    assert pred.get_truth_value(circuit, data) == (XGate() in gate_set)
+
+
+def test_single_physical_mq_not_in() -> None:
+    pred = SinglePhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={XGate()})
+    assert pred.get_truth_value(circuit, data)
+
+
+def test_single_physical_mq_in() -> None:
+    pred = SinglePhysicalPredicate()
+    circuit = Circuit(2)
+    circuit.append_gate(XGate(), 0)
+    circuit.append_gate(CXGate(), (0, 1))
+    data = PassData(circuit)
+    data.model = MachineModel(2, gate_set={XGate(), CXGate()})
+    assert pred.get_truth_value(circuit, data)
diff --git a/tests/passes/control/predicates/test_width.py b/tests/passes/control/predicates/test_width.py
new file mode 100644
index 000000000..5816510e2
--- /dev/null
+++ b/tests/passes/control/predicates/test_width.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from hypothesis import given
+from hypothesis.strategies import integers
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicates.width import WidthPredicate
+
+
+@given(integers(0, 32), integers(1, 32))
+def test_width_predicate(x: int, y: int) -> None:
+    pred = WidthPredicate(x)
+    assert pred.get_truth_value(Circuit(y), PassData(Circuit(y))) == (y < x)
diff --git a/tests/passes/control/test_dothendecide.py b/tests/passes/control/test_dothendecide.py
index 2ad5a40ba..19a71cd87 100644
--- a/tests/passes/control/test_dothendecide.py
+++ b/tests/passes/control/test_dothendecide.py
@@ -1,52 +1,44 @@
 from __future__ import annotations
 
-from typing import Any
-
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.constant.cx import CNOTGate
 from bqskit.passes.control import DoThenDecide
 
 
-def test_dothendecide_accept() -> None:
-    def accept_always(c1: Circuit, c2: Circuit) -> bool:
-        return True
+def accept_always(c1: Circuit, c2: Circuit) -> bool:
+    return True
 
-    class AddCNOTPass(BasePass):
-        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-            circuit.append_gate(CNOTGate(), (0, 1))
 
-    circuit = Circuit(2)
-    dtd_pass = DoThenDecide(accept_always, AddCNOTPass())
-    dtd_pass.run(circuit)
-    assert circuit.num_operations == 1
-    assert circuit.gate_set == {CNOTGate()}
+def reject_always(c1: Circuit, c2: Circuit) -> bool:
+    return False
 
 
-def test_dothendecide_reject() -> None:
-    def reject_always(c1: Circuit, c2: Circuit) -> bool:
-        return False
+class AddCNOTPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(CNOTGate(), (0, 1))
 
-    class AddCNOTPass(BasePass):
-        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-            circuit.append_gate(CNOTGate(), (0, 1))
 
+def test_dothendecide_accept(compiler: Compiler) -> None:
     circuit = Circuit(2)
-    dtd_pass = DoThenDecide(reject_always, AddCNOTPass())
-    dtd_pass.run(circuit)
-    assert circuit.num_operations == 0
+    dtd_pass = DoThenDecide(accept_always, AddCNOTPass())
+    out_circuit = compiler.compile(circuit, dtd_pass)
+    assert out_circuit.num_operations == 1
+    assert out_circuit.gate_set == {CNOTGate()}
 
 
-def test_dothendecide_list() -> None:
-    def accept_always(c1: Circuit, c2: Circuit) -> bool:
-        return True
+def test_dothendecide_reject(compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    dtd_pass = DoThenDecide(reject_always, AddCNOTPass())
+    out_circuit = compiler.compile(circuit, dtd_pass)
+    assert out_circuit.num_operations == 0
 
-    class AddCNOTPass(BasePass):
-        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-            circuit.append_gate(CNOTGate(), (0, 1))
 
+def test_dothendecide_list(compiler: Compiler) -> None:
     circuit = Circuit(2)
     dtd_pass = DoThenDecide(accept_always, [AddCNOTPass(), AddCNOTPass()])
-    dtd_pass.run(circuit)
-    assert circuit.num_operations == 2
-    assert circuit.gate_set == {CNOTGate()}
+    out_circuit = compiler.compile(circuit, dtd_pass)
+    assert out_circuit.num_operations == 2
+    assert out_circuit.gate_set == {CNOTGate()}
diff --git a/tests/passes/control/test_dowhileloop.py b/tests/passes/control/test_dowhileloop.py
index e69de29bb..67e5df36d 100644
--- a/tests/passes/control/test_dowhileloop.py
+++ b/tests/passes/control/test_dowhileloop.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.dowhileloop import DoWhileLoopPass
+from bqskit.passes.control.predicate import PassPredicate
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+class TrueX2Predicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        if '__true_x2_counter' not in data:
+            data['__true_x2_counter'] = 1
+            return True
+
+        data['__true_x2_counter'] += 1
+        return data['__true_x2_counter'] <= 2
+
+
+class TrueX3Predicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        if '__true_x3_counter' not in data:
+            data['__true_x3_counter'] = 1
+            return True
+
+        data['__true_x3_counter'] += 1
+        return data['__true_x3_counter'] <= 3
+
+
+class RunsOnce(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        if '_runs_once_flag' in data:
+            assert False, 'Should never have been executed.'
+
+        data['_runs_once_flag'] = True
+
+
+class RunsThrice(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        if '_runs_thrice_counter' not in data:
+            data['_runs_thrice_counter'] = 1
+            return
+
+        data['_runs_thrice_counter'] += 1
+        if data['_runs_thrice_counter'] > 3:
+            assert False, 'Should never have been executed.'
+
+
+def test_dowhile_runs_once_on_false(compiler: Compiler) -> None:
+    dwl_pass = DoWhileLoopPass(FalsePredicate(), RunsOnce())
+    compiler.compile(Circuit(1), [dwl_pass])
+
+
+def test_dowhile_runs_while_true(compiler: Compiler) -> None:
+    dwl_pass = DoWhileLoopPass(TrueX2Predicate(), RunsThrice())
+    compiler.compile(Circuit(1), [dwl_pass])
+
+
+def test_dowhile_runs_while_true_xfail(fresh_compiler: Compiler) -> None:
+    dwl_pass = DoWhileLoopPass(TrueX3Predicate(), RunsThrice())
+    with pytest.raises(RuntimeError):
+        fresh_compiler.compile(Circuit(1), [dwl_pass])
diff --git a/tests/passes/control/test_foreachblock.py b/tests/passes/control/test_foreachblock.py
new file mode 100644
index 000000000..605392b85
--- /dev/null
+++ b/tests/passes/control/test_foreachblock.py
@@ -0,0 +1,132 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CircuitGate
+from bqskit.ir.gates import HGate
+from bqskit.ir.gates import XGate
+from bqskit.ir.gates import YGate
+from bqskit.ir.gates import ZGate
+from bqskit.ir.operation import Operation
+from bqskit.passes import UnfoldPass
+from bqskit.passes.control.foreach import ForEachBlockPass
+
+
+@pytest.fixture
+def blk_circuit() -> Circuit:
+    circuit = Circuit(3)
+
+    for gate, loc in [(XGate(), (0, 1)), (YGate(), (1, 2)), (ZGate(), (0, 1))]:
+        block = Circuit(2)
+        block.append_gate(gate, 0)
+        block.append_gate(gate, 1)
+        block_gate = CircuitGate(block)
+
+        circuit.append_gate(block_gate, loc)
+
+    return circuit
+
+
+class RecordGateSetPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        data['gate_set'] = circuit.gate_set
+
+
+class ReplaceXwithHPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        if XGate() in circuit.gate_set:
+            circuit.pop()
+            circuit.pop()
+            circuit.append_gate(HGate(), 0)
+            circuit.append_gate(HGate(), 1)
+            circuit.append_gate(HGate(), 0)
+            circuit.append_gate(HGate(), 1)
+
+
+def never_replace(c: Circuit, o: Operation) -> bool:
+    return False
+
+
+class RemoveXGatePass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        if XGate() in circuit.gate_set:
+            circuit.pop()
+            circuit.pop()
+
+
+def only_x(o: Operation) -> bool:
+    return o.gate == XGate()
+
+
+class AssertXGatePass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        assert XGate() in circuit.gate_set
+        assert len(circuit.gate_set) == 1
+
+
+def empty_coll(_: Operation) -> bool:
+    return False
+
+
+def test_every_block_touched(blk_circuit: Circuit, compiler: Compiler) -> None:
+    feb_pass = ForEachBlockPass(RecordGateSetPass())
+    _, out_data = compiler.compile(blk_circuit, feb_pass, True)
+    feb_data = out_data[ForEachBlockPass.key]
+    assert len(feb_data) == 1
+    assert len(feb_data[0]) == 3
+    touched_gates = set()
+    for x in feb_data[0]:
+        assert len(x['gate_set']) == 1
+        touched_gates.update(x['gate_set'])
+    assert len(touched_gates) == 3
+    assert all(g in touched_gates for g in [XGate(), YGate(), ZGate()])
+
+
+def test_replacement_happens(blk_circuit: Circuit, compiler: Compiler) -> None:
+    feb_pass = ForEachBlockPass(ReplaceXwithHPass())
+    out_circuit = compiler.compile(blk_circuit, [feb_pass, UnfoldPass()])
+    assert len(out_circuit) == 8
+    assert len(out_circuit.gate_set) == 3
+    assert all(g in out_circuit.gate_set for g in [HGate(), YGate(), ZGate()])
+
+
+def test_replace_filter(blk_circuit: Circuit, compiler: Compiler) -> None:
+    feb_pass = ForEachBlockPass(
+        ReplaceXwithHPass(), replace_filter=never_replace,
+    )
+    out_circuit = compiler.compile(blk_circuit, [feb_pass, UnfoldPass()])
+    assert len(out_circuit) == 6
+    assert len(out_circuit.gate_set) == 3
+    assert all(g in out_circuit.gate_set for g in [XGate(), YGate(), ZGate()])
+
+
+def test_calculate_error(blk_circuit: Circuit, compiler: Compiler) -> None:
+    in_utry = blk_circuit.get_unitary()
+    feb_pass = ForEachBlockPass(RemoveXGatePass(), calculate_error_bound=True)
+    workflow = [feb_pass, UnfoldPass()]
+    out_circuit, data = compiler.compile(blk_circuit, workflow, True)
+    out_utry = out_circuit.get_unitary()
+    dist = out_utry.get_distance_from(in_utry)
+    assert data.error == dist
+
+
+def test_collection_filter(blk_circuit: Circuit, compiler: Compiler) -> None:
+    blk_circuit.unfold_all()
+    feb_pass = ForEachBlockPass(AssertXGatePass(), collection_filter=only_x)
+    compiler.compile(blk_circuit, feb_pass)
+
+
+def test_no_hang_on_empty_circuit(compiler: Compiler) -> None:
+    feb_pass = ForEachBlockPass(RemoveXGatePass(), collection_filter=empty_coll)
+    compiler.compile(Circuit(1), feb_pass)
+
+
+def test_no_hang_on_empty_collection(compiler: Compiler) -> None:
+    circuit = Circuit(1)
+    circuit.append_gate(XGate(), 0)
+    feb_pass = ForEachBlockPass(RemoveXGatePass(), collection_filter=empty_coll)
+    compiler.compile(circuit, feb_pass)
diff --git a/tests/passes/control/test_ifthenelse.py b/tests/passes/control/test_ifthenelse.py
new file mode 100644
index 000000000..caec18713
--- /dev/null
+++ b/tests/passes/control/test_ifthenelse.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CXGate
+from bqskit.passes.control.ifthenelse import IfThenElsePass
+from bqskit.passes.control.predicate import PassPredicate
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+class TruePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return True
+
+
+class AddCXGate(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(CXGate(), (0, 1))
+
+
+class NeverRuns(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        assert False, 'Should never have been executed.'
+
+
+def test_ifthenelse_on_true(compiler: Compiler) -> None:
+    ite_pass = IfThenElsePass(TruePredicate(), AddCXGate())
+    out_circuit = compiler.compile(Circuit(2), ite_pass)
+    assert CXGate() in out_circuit.gate_set
+
+
+def test_ifthenelse_on_true_with_false(compiler: Compiler) -> None:
+    ite_pass = IfThenElsePass(TruePredicate(), AddCXGate(), NeverRuns())
+    out_circuit = compiler.compile(Circuit(2), ite_pass)
+    assert CXGate() in out_circuit.gate_set
+
+
+def test_ifthenelse_on_false(compiler: Compiler) -> None:
+    ite_pass = IfThenElsePass(FalsePredicate(), NeverRuns(), AddCXGate())
+    out_circuit = compiler.compile(Circuit(2), ite_pass)
+    assert CXGate() in out_circuit.gate_set
diff --git a/tests/passes/control/test_paralleldo.py b/tests/passes/control/test_paralleldo.py
new file mode 100644
index 000000000..2d69b5e70
--- /dev/null
+++ b/tests/passes/control/test_paralleldo.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import time
+
+import pytest
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import XGate
+from bqskit.ir.gates import YGate
+from bqskit.ir.gates import ZGate
+from bqskit.passes.control.paralleldo import ParallelDo
+
+
+class AddXGate(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(XGate(), 0)
+        data['key'] = 'x'
+
+
+class AddYGate(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(YGate(), 0)
+        data['key'] = 'y'
+
+
+class AddZGate(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(ZGate(), 0)
+        data['key'] = 'z'
+
+
+class Sleep1Pass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        time.sleep(0.1)
+        data['key'] = '1'
+
+
+class Sleep3Pass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        circuit.append_gate(ZGate(), 0)
+        time.sleep(0.3)
+        data['key'] = '3'
+
+
+def pick_z(c1: Circuit, c2: Circuit) -> bool:
+    if ZGate() in c1.gate_set:
+        return True
+    else:
+        return False
+
+
+def test_parallel_do_chooses_correct_result(compiler: Compiler) -> None:
+    pd_pass = ParallelDo([[AddXGate()], [AddYGate()], [AddZGate()]], pick_z)
+    out_circuit, data = compiler.compile(Circuit(1), pd_pass, True)
+    assert ZGate() in out_circuit.gate_set
+    assert len(out_circuit.gate_set) == 1
+    assert data['key'] == 'z'
+
+
+def test_parallel_do_no_passes() -> None:
+    with pytest.raises(ValueError):
+        ParallelDo([], pick_z)
+
+
+def test_parallel_do_pick_first(compiler: Compiler) -> None:
+    passes: list[list[BasePass]] = [[Sleep3Pass()], [Sleep1Pass()]]
+    pd_pass = ParallelDo(passes, pick_z, True)
+    _, data = compiler.compile(Circuit(1), pd_pass, True)
+    assert data['key'] == '1'
diff --git a/tests/passes/control/test_whileloop.py b/tests/passes/control/test_whileloop.py
new file mode 100644
index 000000000..cef893516
--- /dev/null
+++ b/tests/passes/control/test_whileloop.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+from bqskit.passes.control.whileloop import WhileLoopPass
+
+
+class FalsePredicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        return False
+
+
+class TrueX3Predicate(PassPredicate):
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        if '__true_x3_counter' not in data:
+            data['__true_x3_counter'] = 1
+            return True
+
+        data['__true_x3_counter'] += 1
+        return data['__true_x3_counter'] <= 3
+
+
+class NeverRuns(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        assert False, 'Should never have been executed.'
+
+
+class RunsThrice(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        if '_runs_thrice_counter' not in data:
+            data['_runs_thrice_counter'] = 1
+            return
+
+        data['_runs_thrice_counter'] += 1
+        if data['_runs_thrice_counter'] > 3:
+            assert False, 'Should never have been executed.'
+
+
+def test_while_never_runs_on_false(compiler: Compiler) -> None:
+    wl_pass = WhileLoopPass(FalsePredicate(), NeverRuns())
+    compiler.compile(Circuit(1), wl_pass)
+
+
+def test_while_runs_while_true(compiler: Compiler) -> None:
+    wl_pass = WhileLoopPass(TrueX3Predicate(), RunsThrice())
+    compiler.compile(Circuit(1), wl_pass)
diff --git a/tests/passes/mapping/__init__.py b/tests/passes/mapping/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/mapping/test_apply.py b/tests/passes/mapping/test_apply.py
index d47f43f4d..77e26af36 100644
--- a/tests/passes/mapping/test_apply.py
+++ b/tests/passes/mapping/test_apply.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
-from typing import Any
-
+from bqskit.compiler import Compiler
 from bqskit.compiler import MachineModel
 from bqskit.ir import Circuit
 from bqskit.passes import ApplyPlacement
@@ -11,10 +10,8 @@
 
 def test_apply_placement(r3_qubit_circuit: Circuit) -> None:
     model = MachineModel(6, [(0, 1), (1, 2), (2, 3), (2, 4), (2, 5), (3, 5)])
-    data: dict[str, Any] = {}
-    SetModelPass(model).run(r3_qubit_circuit, data)
-    GreedyPlacementPass().run(r3_qubit_circuit, data)
-    print(data)
-    ApplyPlacement().run(r3_qubit_circuit, data)
-    assert r3_qubit_circuit.num_qudits == 6
-    assert r3_qubit_circuit.active_qudits == [2, 3, 5]
+    workflow = [SetModelPass(model), GreedyPlacementPass(), ApplyPlacement()]
+    with Compiler() as compiler:
+        out_circuit = compiler.compile(r3_qubit_circuit, workflow)
+        assert out_circuit.num_qudits == 6
+        assert out_circuit.active_qudits == [2, 3, 5]
diff --git a/tests/passes/mapping/test_local_min.py b/tests/passes/mapping/test_local_min.py
index 85dbfdfc4..e071eb632 100644
--- a/tests/passes/mapping/test_local_min.py
+++ b/tests/passes/mapping/test_local_min.py
@@ -70,7 +70,7 @@ def test_escape_min_route(swaps: int, local_mins: int) -> None:
     circuit = looping_circuit(swaps, local_mins)
     cg = CouplingGraph.linear(circuit.num_qudits)
     data = {'machine_model': MachineModel(circuit.num_qudits, cg)}
-    GeneralizedSabreRoutingPass().run(circuit, data)
+    circuit.perform(GeneralizedSabreRoutingPass(), data)
     assert all(e in cg for e in circuit.coupling_graph)
 
 
@@ -82,4 +82,4 @@ def test_escape_min_layout(swaps: int, local_mins: int) -> None:
     circuit = looping_circuit(swaps, local_mins)
     cg = CouplingGraph.linear(circuit.num_qudits)
     data = {'machine_model': MachineModel(circuit.num_qudits, cg)}
-    GeneralizedSabreLayoutPass(3).run(circuit, data)
+    circuit.perform(GeneralizedSabreLayoutPass(3), data)
diff --git a/tests/passes/mapping/test_sabre.py b/tests/passes/mapping/test_sabre.py
index 769e19f0d..bd9bcbf06 100644
--- a/tests/passes/mapping/test_sabre.py
+++ b/tests/passes/mapping/test_sabre.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.compiler import MachineModel
 from bqskit.ir import Circuit
@@ -14,7 +13,7 @@
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
-def test_simple(compiler: Compiler) -> None:
+def test_simple() -> None:
     cg = (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7)
     model = MachineModel(8, cg)
     circuit = Circuit(5)
@@ -34,20 +33,18 @@ def test_simple(compiler: Compiler) -> None:
 
     in_utry = circuit.get_unitary()
 
-    task = CompilationTask(
-        circuit,
-        [
-            SetModelPass(model),
-            GreedyPlacementPass(),
-            GeneralizedSabreLayoutPass(),
-            GeneralizedSabreRoutingPass(),
-        ],
-    )
+    workflow = [
+        SetModelPass(model),
+        GreedyPlacementPass(),
+        GeneralizedSabreLayoutPass(),
+        GeneralizedSabreRoutingPass(),
+    ]
 
-    cc = compiler.compile(task)
-    pi = compiler.analyze(task, 'initial_mapping')
-    pf = compiler.analyze(task, 'final_mapping')
-    PI = PermutationMatrix.from_qubit_location(5, pi)
-    PF = PermutationMatrix.from_qubit_location(5, pf)
-    assert cc.get_unitary().get_distance_from(PF.T @ in_utry @ PI) < 1e-7
-    assert all(e in cg for e in cc.coupling_graph)
+    with Compiler() as compiler:
+        cc, data = compiler.compile(circuit, workflow, True)
+        pi = data['initial_mapping']
+        pf = data['final_mapping']
+        PI = PermutationMatrix.from_qubit_location(5, pi)
+        PF = PermutationMatrix.from_qubit_location(5, pf)
+        assert cc.get_unitary().get_distance_from(PF.T @ in_utry @ PI) < 1e-7
+        assert all(e in cg for e in cc.coupling_graph)
diff --git a/tests/passes/partitioning/__init__.py b/tests/passes/partitioning/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/partitioning/test_cluster.py b/tests/passes/partitioning/test_cluster.py
index 33d0898c3..2cf56f0ec 100644
--- a/tests/passes/partitioning/test_cluster.py
+++ b/tests/passes/partitioning/test_cluster.py
@@ -8,7 +8,7 @@
 class TestClusteringPartitioner:
     def test_run_r6(self, r6_qudit_circuit: Circuit) -> None:
         utry = r6_qudit_circuit.get_unitary()
-        ClusteringPartitioner(3, 2).run(r6_qudit_circuit, {})
+        r6_qudit_circuit.perform(ClusteringPartitioner(3, 2))
 
         assert any(
             isinstance(op.gate, CircuitGate)
diff --git a/tests/passes/partitioning/test_greedy.py b/tests/passes/partitioning/test_greedy.py
deleted file mode 100644
index b30d1bcfa..000000000
--- a/tests/passes/partitioning/test_greedy.py
+++ /dev/null
@@ -1,588 +0,0 @@
-from __future__ import annotations
-
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CircuitGate
-from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
-from bqskit.passes.partitioning.greedy import GreedyPartitioner
-from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
-
-
-class TestGreedyPartitioner:
-    def test_run_r6(self, r6_qudit_circuit: Circuit) -> None:
-        utry = r6_qudit_circuit.get_unitary()
-        GreedyPartitioner(3).run(r6_qudit_circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in r6_qudit_circuit
-        )
-        assert r6_qudit_circuit.get_unitary() == utry
-        for cycle_index in range(r6_qudit_circuit.num_cycles):
-            assert not r6_qudit_circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_1(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 4,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 0,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                2, 0, 3, 5,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_2(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                3, 0,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 0,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                4, 0, 1, 2,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                5, 0, 1,
-            ],
-        )
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_3(self) -> None:
-        circuit = Circuit(6)
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                5, 1, 3,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 3,
-            ],
-        )
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_4(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 0,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                2, 0,
-            ],
-        )
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 3,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 0,
-            ],
-        )
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_5(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                4, 1, 2, 3,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                2, 1,
-            ],
-        )
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_6(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 5,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                4, 0, 2,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                0, 2, 3, 4,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 5,
-            ],
-        )
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_7(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                0, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                0, 1, 3,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 0,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                0, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                4, 0, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                1, 0, 3,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 0,
-            ],
-        )
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_8(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 0,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                1, 0, 2,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                5, 0, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 0,
-            ],
-        )
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_9(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(3),
-            ), [
-                2, 1, 4,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                5, 1,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                5, 1, 2, 4,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 1,
-            ],
-        )
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
-
-    def test_corner_case_10(self) -> None:
-        circuit = Circuit(6)
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                1, 2,
-            ],
-        )
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                4, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(2),
-            ), [
-                3, 1,
-            ],
-        )
-
-        circuit.append_gate(
-            ConstantUnitaryGate(
-                UnitaryMatrix.random(4),
-            ), [
-                1, 2, 3, 4,
-            ],
-        )
-
-        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
-
-        utry = circuit.get_unitary()
-        GreedyPartitioner(3).run(circuit, {})
-
-        assert all(
-            isinstance(op.gate, CircuitGate)
-            or len(op.location) > 3
-            for op in circuit
-        )
-        assert circuit.get_unitary() == utry
-        for cycle_index in range(circuit.num_cycles):
-            assert not circuit._is_cycle_idle(cycle_index)
diff --git a/tests/passes/partitioning/test_parts.py b/tests/passes/partitioning/test_parts.py
index ba3e06db2..f9c6a8e46 100644
--- a/tests/passes/partitioning/test_parts.py
+++ b/tests/passes/partitioning/test_parts.py
@@ -11,8 +11,14 @@
 from bqskit.passes import UnfoldPass
 
 
+if os.path.isdir(os.path.join(os.path.dirname(__file__), '_data')):
+    params = os.listdir(os.path.join(os.path.dirname(__file__), '_data'))
+else:
+    params = []
+
+
 @pytest.fixture(
-    params=os.listdir(os.path.join(os.path.dirname(__file__), '_data')),
+    params=params,
     ids=lambda qasm_file: os.path.splitext(os.path.basename(qasm_file))[0],
 )
 def big_qasm_file(request: Any) -> str:
@@ -25,8 +31,8 @@ def big_qasm_file(request: Any) -> str:
 def test_parters(big_qasm_file: str) -> None:
     c = Circuit.from_file(big_qasm_file)
     wc = c.copy()
-    QuickPartitioner(3).run(wc)
-    UnfoldPass().run(wc)
+    wc.perform(QuickPartitioner(3))
+    wc.perform(UnfoldPass())
 
     for q in range(c.num_qudits):
         gate_list1 = list(c.operations_with_cycles(qudits_or_region=[q]))
diff --git a/tests/passes/partitioning/test_quick.py b/tests/passes/partitioning/test_quick.py
index 473ed955c..619df2899 100644
--- a/tests/passes/partitioning/test_quick.py
+++ b/tests/passes/partitioning/test_quick.py
@@ -11,7 +11,7 @@
 class TestQuickPartitioner:
     def test_run_r6(self, r6_qudit_circuit: Circuit) -> None:
         utry = r6_qudit_circuit.get_unitary()
-        QuickPartitioner(3).run(r6_qudit_circuit, {})
+        r6_qudit_circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -58,7 +58,7 @@ def test_corner_case_1(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -113,7 +113,7 @@ def test_corner_case_2(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -165,7 +165,7 @@ def test_corner_case_3(self) -> None:
             ],
         )
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -218,7 +218,7 @@ def test_corner_case_4(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -280,7 +280,7 @@ def test_corner_case_5(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -340,7 +340,7 @@ def test_corner_case_6(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -414,7 +414,7 @@ def test_corner_case_7(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -469,7 +469,7 @@ def test_corner_case_8(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -522,7 +522,7 @@ def test_corner_case_9(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -577,7 +577,7 @@ def test_corner_case_10(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -598,7 +598,7 @@ def test_corner_case_11(self) -> None:
         circuit.append_gate(CNOTGate(), (1, 2))
 
         utry = circuit.get_unitary()
-        QuickPartitioner(3).run(circuit, {})
+        circuit.perform(QuickPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
diff --git a/tests/passes/partitioning/test_scan.py b/tests/passes/partitioning/test_scan.py
index 43cbe6eae..6925dc5c9 100644
--- a/tests/passes/partitioning/test_scan.py
+++ b/tests/passes/partitioning/test_scan.py
@@ -33,7 +33,7 @@ def test_run(self) -> None:
         circ.append_gate(CNOTGate(), [1, 2])
         circ.append_gate(CNOTGate(), [2, 3])
         utry = circ.get_unitary()
-        ScanPartitioner(3).run(circ, {})
+        circ.perform(ScanPartitioner(3))
 
         assert len(circ) == 3
         assert all(isinstance(op.gate, CircuitGate) for op in circ)
@@ -45,7 +45,7 @@ def test_run(self) -> None:
 
     def test_run_r6(self, r6_qudit_circuit: Circuit) -> None:
         utry = r6_qudit_circuit.get_unitary()
-        ScanPartitioner(3).run(r6_qudit_circuit, {})
+        r6_qudit_circuit.perform(ScanPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -102,7 +102,7 @@ def test_corner_case_3(self) -> None:
             ],
         )
         utry = circuit.get_unitary()
-        ScanPartitioner(3).run(circuit, {})
+        circuit.perform(ScanPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -160,7 +160,7 @@ def test_corner_case_4(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        ScanPartitioner(3).run(circuit, {})
+        circuit.perform(ScanPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -239,7 +239,7 @@ def test_corner_case_7(self) -> None:
         )
 
         utry = circuit.get_unitary()
-        ScanPartitioner(3).run(circuit, {})
+        circuit.perform(ScanPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -299,7 +299,7 @@ def test_corner_case_8(self) -> None:
         circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
 
         utry = circuit.get_unitary()
-        ScanPartitioner(3).run(circuit, {})
+        circuit.perform(ScanPartitioner(3))
 
         assert all(
             isinstance(op.gate, CircuitGate)
@@ -333,4 +333,4 @@ def test_bigger_block(self) -> None:
             ],
         )
         with pytest.raises(RuntimeError):
-            ScanPartitioner(3).run(circuit)
+            circuit.perform(ScanPartitioner(3))
diff --git a/tests/passes/partitioning/test_single.py b/tests/passes/partitioning/test_single.py
index 9cf994d7a..192f11922 100644
--- a/tests/passes/partitioning/test_single.py
+++ b/tests/passes/partitioning/test_single.py
@@ -7,7 +7,7 @@
 
 
 def test_single_qudit_grouper(r6_qudit_circuit: Circuit) -> None:
-    GroupSingleQuditGatePass().run(r6_qudit_circuit)
+    r6_qudit_circuit.perform(GroupSingleQuditGatePass())
 
     # All single-qudit gates should be in a CircuitGate
     for gate in r6_qudit_circuit.gate_set:
diff --git a/tests/passes/processing/__init__.py b/tests/passes/processing/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/processing/test_substitute.py b/tests/passes/processing/test_substitute.py
index 0b1f6e2f7..2a51f6e4b 100644
--- a/tests/passes/processing/test_substitute.py
+++ b/tests/passes/processing/test_substitute.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import VariableUnitaryGate
@@ -9,6 +8,10 @@
 from bqskit.qis import UnitaryMatrix
 
 
+def is_variable(op: Operation) -> bool:
+    return isinstance(op.gate, VariableUnitaryGate)
+
+
 class TestSubstitute:
     def test_small_qubit(self) -> None:
         utry = UnitaryMatrix.identity(4)
@@ -17,27 +20,24 @@ def test_small_qubit(self) -> None:
         circuit.instantiate(utry)
         assert circuit.get_unitary().get_distance_from(utry) < 1e-5
 
-        def is_variable(op: Operation) -> bool:
-            return isinstance(op.gate, VariableUnitaryGate)
         substitute = SubstitutePass(is_variable, VariableUnitaryGate(1))
-        substitute.run(circuit)
+        circuit.perform(substitute)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
         assert circuit.num_operations == 1
         assert circuit[0, 0].num_qudits == 1
 
-    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.identity(4)
-        circuit = Circuit(2)
-        circuit.append_gate(VariableUnitaryGate(2), [0, 1])
-        circuit.instantiate(utry)
-        assert circuit.get_unitary().get_distance_from(utry) < 1e-5
+    def test_small_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.identity(4)
+            circuit = Circuit(2)
+            circuit.append_gate(VariableUnitaryGate(2), [0, 1])
+            circuit.instantiate(utry)
+            assert circuit.get_unitary().get_distance_from(utry) < 1e-5
 
-        def is_variable(op: Operation) -> bool:
-            return isinstance(op.gate, VariableUnitaryGate)
-        substitute = SubstitutePass(is_variable, VariableUnitaryGate(1))
-        circuit = compiler.compile(CompilationTask(circuit, [substitute]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-5
-        assert circuit.num_operations == 1
-        assert circuit[0, 0].num_qudits == 1
+            substitute = SubstitutePass(is_variable, VariableUnitaryGate(1))
+            circuit = compiler.compile(circuit, [substitute])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-5
+            assert circuit.num_operations == 1
+            assert circuit[0, 0].num_qudits == 1
diff --git a/tests/passes/rules/__init__.py b/tests/passes/rules/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/rules/test_cnot2cz.py b/tests/passes/rules/test_cnot2cz.py
index 1a1b3943b..63fe9f392 100644
--- a/tests/passes/rules/test_cnot2cz.py
+++ b/tests/passes/rules/test_cnot2cz.py
@@ -14,7 +14,7 @@ def test_cnot2cz_only_cnots() -> None:
         circuit.append_gate(CNOTGate(), (1, 2))
 
     utry = circuit.get_unitary()
-    CNOTToCZPass().run(circuit)
+    circuit.perform(CNOTToCZPass())
     assert CNOTGate() not in circuit.gate_set
     assert CZGate() in circuit.gate_set
     assert circuit.get_unitary().get_distance_from(utry) < 5e-8
@@ -35,7 +35,7 @@ def test_cnot2cz_with_single_qubit() -> None:
         circuit.append_gate(U3Gate(), 2)
 
     utry = circuit.get_unitary()
-    CNOTToCZPass().run(circuit)
+    circuit.perform(CNOTToCZPass())
     assert CNOTGate() not in circuit.gate_set
     assert CZGate() in circuit.gate_set
     assert circuit.get_unitary().get_distance_from(utry) < 5e-8
diff --git a/tests/passes/rules/test_zxzxz.py b/tests/passes/rules/test_zxzxz.py
index 51c767af8..6de3721aa 100644
--- a/tests/passes/rules/test_zxzxz.py
+++ b/tests/passes/rules/test_zxzxz.py
@@ -1,13 +1,14 @@
 from __future__ import annotations
 
+from bqskit.compiler.compiler import Compiler
 from bqskit.ir import Circuit
 from bqskit.passes import ZXZXZDecomposition
 from bqskit.qis import UnitaryMatrix
 
 
-def test_zxzxz_decomposition() -> None:
-    for i in range(1000):
+def test_zxzxz_decomposition(compiler: Compiler) -> None:
+    for i in range(100):
         test_utry = UnitaryMatrix.random(1)
         test_circuit = Circuit.from_unitary(test_utry)
-        ZXZXZDecomposition().run(test_circuit)
-        assert test_circuit.get_unitary().get_distance_from(test_utry) < 5e-8
+        out_circuit = compiler.compile(test_circuit, ZXZXZDecomposition())
+        assert out_circuit.get_unitary().get_distance_from(test_utry) < 5e-8
diff --git a/tests/passes/search/__init__.py b/tests/passes/search/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/search/generators/__init__.py b/tests/passes/search/generators/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/synthesis/__init__.py b/tests/passes/synthesis/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/synthesis/test_leap.py b/tests/passes/synthesis/test_leap.py
index a1e685b86..0933145cc 100644
--- a/tests/passes/synthesis/test_leap.py
+++ b/tests/passes/synthesis/test_leap.py
@@ -1,13 +1,12 @@
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.constant.cpi import CPIGate
+from bqskit.ir.gates.parameterized.u8 import U8Gate
 from bqskit.passes import LEAPSynthesisPass
+from bqskit.passes.search.generators.simple import SimpleLayerGenerator
 from bqskit.qis import UnitaryMatrix
-# from bqskit.ir.gates import CPIGate
-# from bqskit.ir.gates import U8Gate
-# from bqskit.passes import SimpleLayerGenerator
 
 
 class TestLeap:
@@ -15,23 +14,24 @@ def test_small_qubit(self) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         leap = LEAPSynthesisPass()
-        leap.run(circuit)
+        circuit.perform(leap)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(2)
-        circuit = Circuit.from_unitary(utry)
-        leap = LEAPSynthesisPass()
-        circuit = compiler.compile(CompilationTask(circuit, [leap]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-5
+    def test_small_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(2)
+            circuit = Circuit.from_unitary(utry)
+            leap = LEAPSynthesisPass()
+            circuit = compiler.compile(circuit, [leap])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-5
 
-    # def test_small_qutrit(self) -> None:
-    #     utry = UnitaryMatrix.random(2, [3, 3])
-    #     circuit = Circuit.from_unitary(utry)
-    #     layer_gen = SimpleLayerGenerator(CPIGate(), U8Gate())
-    #     qsearch = QSearchSynthesisPass(layer_generator=layer_gen)
-    #     qsearch.run(circuit)
-    #     dist = circuit.get_unitary().get_distance_from(utry)
-    #     assert dist <= qsearch.success_threshold
+    def test_small_qutrit(self) -> None:
+        utry = UnitaryMatrix.random(2, [3, 3])
+        circuit = Circuit.from_unitary(utry)
+        layer_gen = SimpleLayerGenerator(CPIGate(), U8Gate())
+        leap = LEAPSynthesisPass(layer_generator=layer_gen)
+        circuit.perform(leap)
+        dist = circuit.get_unitary().get_distance_from(utry, 1)
+        assert dist <= leap.success_threshold
diff --git a/tests/passes/synthesis/test_qfast.py b/tests/passes/synthesis/test_qfast.py
index e32c80e8e..7fb06ee86 100644
--- a/tests/passes/synthesis/test_qfast.py
+++ b/tests/passes/synthesis/test_qfast.py
@@ -1,8 +1,5 @@
 from __future__ import annotations
 
-import pytest
-
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import CircuitGate
@@ -17,35 +14,36 @@ def test_small_qubit(self) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         qfast = QFASTDecompositionPass()
-        qfast.run(circuit)
+        circuit.perform(qfast)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(2)
-        circuit = Circuit.from_unitary(utry)
-        qfast = QFASTDecompositionPass()
-        circuit = compiler.compile(CompilationTask(circuit, [qfast]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-5
+    def test_small_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(2)
+            circuit = Circuit.from_unitary(utry)
+            qfast = QFASTDecompositionPass()
+            circuit = compiler.compile(circuit, [qfast])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-5
 
     def test_3_qubit(self) -> None:
         utry = UnitaryMatrix.random(3)
         circuit = Circuit.from_unitary(utry)
         qfast = QFASTDecompositionPass()
-        qfast.run(circuit)
+        circuit.perform(qfast)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_3_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(3)
-        circuit = Circuit.from_unitary(utry)
-        qfast = QFASTDecompositionPass()
-        circuit = compiler.compile(CompilationTask(circuit, [qfast]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-5
+    def test_3_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(3)
+            circuit = Circuit.from_unitary(utry)
+            qfast = QFASTDecompositionPass()
+            circuit = compiler.compile(circuit, [qfast])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-5
 
-    @pytest.mark.skip(reason='Issue in bqskitrs supporting CircuitGates.')
     def test_3_qubit_with_cnot_block(self) -> None:
         circuit = Circuit(2)
         circuit.append_gate(CNOTGate(), (0, 1))
@@ -56,6 +54,6 @@ def test_3_qubit_with_cnot_block(self) -> None:
         utry = UnitaryMatrix.random(3)
         circuit = Circuit.from_unitary(utry)
         qfast = QFASTDecompositionPass(cg)
-        qfast.run(circuit)
+        circuit.perform(qfast)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
diff --git a/tests/passes/synthesis/test_qpredict.py b/tests/passes/synthesis/test_qpredict.py
index 3637001e1..d0d82ecf0 100644
--- a/tests/passes/synthesis/test_qpredict.py
+++ b/tests/passes/synthesis/test_qpredict.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
 from bqskit.passes import QPredictDecompositionPass
@@ -12,30 +11,32 @@ def test_small_qubit(self) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         qpredict = QPredictDecompositionPass()
-        qpredict.run(circuit)
+        circuit.perform(qpredict)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-3
 
-    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(2)
-        circuit = Circuit.from_unitary(utry)
-        qpredict = QPredictDecompositionPass()
-        circuit = compiler.compile(CompilationTask(circuit, [qpredict]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-3
+    def test_small_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(2)
+            circuit = Circuit.from_unitary(utry)
+            qpredict = QPredictDecompositionPass()
+            circuit = compiler.compile(circuit, [qpredict])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-3
 
     def test_3_qubit(self) -> None:
         utry = UnitaryMatrix.random(3)
         circuit = Circuit.from_unitary(utry)
         qpredict = QPredictDecompositionPass()
-        qpredict.run(circuit)
+        circuit.perform(qpredict)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-3
 
-    def test_3_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(3)
-        circuit = Circuit.from_unitary(utry)
-        qpredict = QPredictDecompositionPass()
-        circuit = compiler.compile(CompilationTask(circuit, [qpredict]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-3
+    def test_3_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(3)
+            circuit = Circuit.from_unitary(utry)
+            qpredict = QPredictDecompositionPass()
+            circuit = compiler.compile(circuit, [qpredict])
+            dist = circuit.get_unitary().get_distance_from(utry)
+            assert dist <= 1e-3
diff --git a/tests/passes/synthesis/test_qsearch.py b/tests/passes/synthesis/test_qsearch.py
index deed27f4c..32be13533 100644
--- a/tests/passes/synthesis/test_qsearch.py
+++ b/tests/passes/synthesis/test_qsearch.py
@@ -1,9 +1,11 @@
 from __future__ import annotations
 
-from bqskit.compiler import CompilationTask
 from bqskit.compiler import Compiler
 from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.constant.cpi import CPIGate
+from bqskit.ir.gates.parameterized.u8 import U8Gate
 from bqskit.passes import QSearchSynthesisPass
+from bqskit.passes.search.generators.simple import SimpleLayerGenerator
 from bqskit.qis import UnitaryMatrix
 # from bqskit.ir.gates import CPIGate
 # from bqskit.ir.gates import U8Gate
@@ -15,23 +17,24 @@ def test_small_qubit(self) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         qsearch = QSearchSynthesisPass()
-        qsearch.run(circuit)
+        circuit.perform(qsearch)
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
-        utry = UnitaryMatrix.random(2)
-        circuit = Circuit.from_unitary(utry)
-        qsearch = QSearchSynthesisPass()
-        circuit = compiler.compile(CompilationTask(circuit, [qsearch]))
-        dist = circuit.get_unitary().get_distance_from(utry)
-        assert dist <= 1e-5
+    def test_small_qubit_with_compiler(self) -> None:
+        with Compiler() as compiler:
+            utry = UnitaryMatrix.random(2)
+            circuit = Circuit.from_unitary(utry)
+            qsearch = QSearchSynthesisPass()
+            circuit = compiler.compile(circuit, [qsearch])
+            dist = circuit.get_unitary().get_distance_from(utry, 1)
+            assert dist <= 1e-5
 
-    # def test_small_qutrit(self) -> None:
-    #     utry = UnitaryMatrix.random(2, [3, 3])
-    #     circuit = Circuit.from_unitary(utry)
-    #     layer_gen = SimpleLayerGenerator(CPIGate(), U8Gate())
-    #     qsearch = QSearchSynthesisPass(layer_generator=layer_gen)
-    #     qsearch.run(circuit)
-    #     dist = circuit.get_unitary().get_distance_from(utry)
-    #     assert dist <= qsearch.success_threshold
+    def test_small_qutrit(self) -> None:
+        utry = UnitaryMatrix.random(2, [3, 3])
+        circuit = Circuit.from_unitary(utry)
+        layer_gen = SimpleLayerGenerator(CPIGate(), U8Gate())
+        leap = QSearchSynthesisPass(layer_generator=layer_gen)
+        circuit.perform(leap)
+        dist = circuit.get_unitary().get_distance_from(utry, 1)
+        assert dist <= leap.success_threshold
diff --git a/tests/passes/util/__init__.py b/tests/passes/util/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/util/test_extend.py b/tests/passes/util/test_extend.py
index 24f07e17f..122f2fbf2 100644
--- a/tests/passes/util/test_extend.py
+++ b/tests/passes/util/test_extend.py
@@ -8,8 +8,8 @@
 
 def test_extend_3(r6_qudit_circuit: Circuit) -> None:
     in_utry = r6_qudit_circuit.get_unitary()
-    QuickPartitioner(3).run(r6_qudit_circuit)
-    ExtendBlockSizePass(3).run(r6_qudit_circuit)
+    r6_qudit_circuit.perform(QuickPartitioner(3))
+    r6_qudit_circuit.perform(ExtendBlockSizePass(3))
     out_utry = r6_qudit_circuit.get_unitary()
     assert all(isinstance(op.gate, CircuitGate) for op in r6_qudit_circuit)
     assert all(op.num_qudits == 3 for op in r6_qudit_circuit)
@@ -18,8 +18,8 @@ def test_extend_3(r6_qudit_circuit: Circuit) -> None:
 
 def test_extend_4(r6_qudit_circuit: Circuit) -> None:
     in_utry = r6_qudit_circuit.get_unitary()
-    QuickPartitioner(3).run(r6_qudit_circuit)
-    ExtendBlockSizePass(4).run(r6_qudit_circuit)
+    r6_qudit_circuit.perform(QuickPartitioner(3))
+    r6_qudit_circuit.perform(ExtendBlockSizePass(4))
     out_utry = r6_qudit_circuit.get_unitary()
     assert all(isinstance(op.gate, CircuitGate) for op in r6_qudit_circuit)
     assert all(op.num_qudits == 4 for op in r6_qudit_circuit)
diff --git a/tests/passes/util/test_random.py b/tests/passes/util/test_random.py
index 8069ac6ee..f01c35659 100644
--- a/tests/passes/util/test_random.py
+++ b/tests/passes/util/test_random.py
@@ -1,11 +1,6 @@
 from __future__ import annotations
 
-from typing import Any
-
-import pytest
-
 from bqskit.compiler import Compiler
-from bqskit.compiler.task import CompilationTask
 from bqskit.ir import Circuit
 from bqskit.ir.gates import CCXGate
 from bqskit.passes import QSearchSynthesisPass
@@ -13,76 +8,26 @@
 from bqskit.qis import UnitaryMatrix
 
 
-def test_two_qubit_syn_with_seed_no_dask() -> None:
+def test_two_qubit_syn_with_seed(compiler: Compiler) -> None:
     in_utry = UnitaryMatrix.random(2)
 
     circ1 = Circuit.from_unitary(in_utry)
-    data: dict[str, Any] = {}
-    SetRandomSeedPass(0).run(circ1, data)
-    QSearchSynthesisPass().run(circ1, data)
-
     circ2 = Circuit.from_unitary(in_utry)
-    data: dict[str, Any] = {}
-    SetRandomSeedPass(0).run(circ2, data)
-    QSearchSynthesisPass().run(circ2, data)
+    workflow = [SetRandomSeedPass(0), QSearchSynthesisPass()]
+    circ1 = compiler.compile(circ1, workflow)
+    circ2 = compiler.compile(circ2, workflow)
 
     for op1, op2 in zip(circ1, circ2):
         assert op1 == op2
 
 
-def test_three_qubit_syn_with_seed_no_dask() -> None:
+def test_three_qubit_syn_with_seed(compiler: Compiler) -> None:
     in_utry = CCXGate().get_unitary()
-
     circ1 = Circuit.from_unitary(in_utry)
-    data: dict[str, Any] = {}
-    SetRandomSeedPass(0).run(circ1, data)
-    QSearchSynthesisPass().run(circ1, data)
-
     circ2 = Circuit.from_unitary(in_utry)
-    data: dict[str, Any] = {}
-    SetRandomSeedPass(0).run(circ2, data)
-    QSearchSynthesisPass().run(circ2, data)
-
-    for op1, op2 in zip(circ1, circ2):
-        assert op1 == op2
-
-
-def test_two_qubit_syn_with_seed_and_dask(compiler: Compiler) -> None:
-    in_utry = UnitaryMatrix.random(2)
-
-    circ1 = Circuit.from_unitary(in_utry)
-    task1 = CompilationTask(
-        circ1, [SetRandomSeedPass(0), QSearchSynthesisPass()],
-    )
-
-    circ2 = Circuit.from_unitary(in_utry)
-    task2 = CompilationTask(
-        circ2, [SetRandomSeedPass(0), QSearchSynthesisPass()],
-    )
-
-    circ1 = compiler.compile(task1)
-    circ2 = compiler.compile(task2)
-
-    for op1, op2 in zip(circ1, circ2):
-        assert op1 == op2
-
-
-@pytest.mark.xfail(reason='Bug #107')
-def test_three_qubit_syn_with_seed_and_dask(compiler: Compiler) -> None:
-    in_utry = CCXGate().get_unitary()
-
-    circ1 = Circuit.from_unitary(in_utry)
-    task1 = CompilationTask(
-        circ1, [SetRandomSeedPass(0), QSearchSynthesisPass()],
-    )
-
-    circ2 = Circuit.from_unitary(in_utry)
-    task2 = CompilationTask(
-        circ2, [SetRandomSeedPass(0), QSearchSynthesisPass()],
-    )
-
-    circ1 = compiler.compile(task1)
-    circ2 = compiler.compile(task2)
+    workflow = [SetRandomSeedPass(0), QSearchSynthesisPass()]
+    circ1 = compiler.compile(circ1, workflow)
+    circ2 = compiler.compile(circ2, workflow)
 
     for op1, op2 in zip(circ1, circ2):
         assert op1 == op2
diff --git a/tests/qis/__init__.py b/tests/qis/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/runtime/__init__.py b/tests/runtime/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py
new file mode 100644
index 000000000..f3a5a77f2
--- /dev/null
+++ b/tests/runtime/conftest.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import signal
+import subprocess
+import sys
+from typing import Any
+from typing import Iterator
+
+import pytest
+
+from bqskit.compiler import Compiler
+
+params = ['attached', 'detached'] if sys.platform != 'win32' else ['attached']
+
+
+@pytest.fixture(params=params)
+def server_compiler(request: Any) -> Iterator[Compiler]:
+    if request.param == 'detached':
+        manager = subprocess.Popen(['bqskit-manager', '-n2', '-i'])
+        server = subprocess.Popen(['bqskit-server', 'localhost', '-i'])
+        compiler = Compiler('localhost')
+    else:
+        compiler = Compiler(num_workers=2, runtime_log_level=1)
+
+    yield compiler
+
+    if request.param == 'detached':
+        compiler.close()
+        server.send_signal(signal.SIGINT)
+        server.wait()
+        manager.wait()
+        manager.kill()
+        server.kill()
+    else:
+        compiler.close()
diff --git a/tests/runtime/test_attached.py b/tests/runtime/test_attached.py
new file mode 100644
index 000000000..12f0df4df
--- /dev/null
+++ b/tests/runtime/test_attached.py
@@ -0,0 +1,172 @@
+"""Checks the attached runtime's process management and ability to cleanup."""
+from __future__ import annotations
+
+import os
+import signal
+import subprocess
+import sys
+import time
+
+import psutil
+import pytest
+
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.runtime import get_runtime
+
+
+@pytest.mark.parametrize('num_workers', [1, -1])
+def test_startup_shutdown_transparently(num_workers: int) -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    compiler = Compiler(num_workers=num_workers)
+    assert compiler.p is not None
+    compiler.__del__()
+    if sys.platform == 'win32':
+        time.sleep(1)
+    out_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    assert in_num_childs == out_num_childs
+
+
+@pytest.mark.parametrize('num_workers', [1, -1])
+def test_cleanup_close(num_workers: int) -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    compiler = Compiler(num_workers=num_workers)
+    compiler.close()
+    if sys.platform == 'win32':
+        time.sleep(1)
+    out_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    assert in_num_childs == out_num_childs
+
+
+@pytest.mark.parametrize('num_workers', [1, -1])
+def test_cleanup_with_clause(num_workers: int) -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    with Compiler(num_workers=num_workers) as _:
+        pass
+    if sys.platform == 'win32':
+        time.sleep(1)
+    out_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    assert in_num_childs == out_num_childs
+
+
+@pytest.mark.parametrize('num_workers', [1, 2, 4])
+def test_create_workers(num_workers: int) -> None:
+    compiler = Compiler(num_workers=num_workers)
+    assert compiler.p is not None
+    assert len(psutil.Process(compiler.p.pid).children()) == num_workers
+    compiler.close()
+
+
+def test_one_thread_per_worker() -> None:
+    # On windows we aren't sure how the threads are handeled
+    if sys.platform == 'win32':
+        return
+
+    compiler = Compiler(num_workers=1)
+    assert compiler.p is not None
+    assert len(psutil.Process(compiler.p.pid).children()) == 1
+    assert psutil.Process(compiler.p.pid).children()[0].num_threads() == 1
+    compiler.close()
+
+
+def test_double_close() -> None:
+    compiler = Compiler(num_workers=1)
+    assert compiler.p is not None
+    compiler.close()
+    assert compiler.p is None
+    compiler.close()
+    assert compiler.p is None
+
+
+def test_interrupt_handling() -> None:
+    if sys.platform == 'win32':
+        sig = signal.SIGTERM
+    else:
+        sig = signal.SIGINT
+
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    p = subprocess.Popen([
+        'python', '-c',
+        """
+        import time
+        from bqskit.compiler import Compiler
+        compiler = Compiler(num_workers=1)
+        time.sleep(10)
+        """,
+    ])
+    p.send_signal(sig)
+
+    if sys.platform != 'win32':
+        out_num_childs = len(
+            psutil.Process(
+                os.getpid(),
+            ).children(recursive=True),
+        )
+        assert in_num_childs + 1 == out_num_childs
+
+    p.wait()
+    out_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    assert in_num_childs == out_num_childs
+
+
+class TestErrorPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        raise RuntimeError('Boo!')
+
+
+def raise_error() -> None:
+    raise RuntimeError('Boo!')
+
+
+class TestNestedErrorPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        await get_runtime().submit(raise_error)
+
+
+class TestWorkerExitPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        exit()
+
+
+def test_errors_shutdown_system() -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    try:
+        compiler = Compiler(num_workers=1)
+        compiler.compile(Circuit(2), [TestErrorPass()])
+    except RuntimeError:
+        if sys.platform == 'win32':
+            time.sleep(1)
+        num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+        assert in_num_childs == num_childs
+    else:
+        assert False, 'No error caught.'
+
+
+def test_errors_shutdown_system_nested() -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    try:
+        compiler = Compiler(num_workers=1)
+        compiler.compile(Circuit(2), [TestNestedErrorPass()])
+    except RuntimeError:
+        if sys.platform == 'win32':
+            time.sleep(1)
+        num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+        assert in_num_childs == num_childs
+    else:
+        assert False, 'No error caught.'
+
+
+def test_worker_fail_shutdown_system() -> None:
+    in_num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+    try:
+        compiler = Compiler(num_workers=2)
+        compiler.compile(Circuit(2), [TestWorkerExitPass()])
+    except RuntimeError:
+        if sys.platform == 'win32':
+            time.sleep(1)
+        num_childs = len(psutil.Process(os.getpid()).children(recursive=True))
+        assert in_num_childs == num_childs
+    else:
+        assert False, 'No error caught.'
diff --git a/tests/runtime/test_cancel.py b/tests/runtime/test_cancel.py
new file mode 100644
index 000000000..6257b47f4
--- /dev/null
+++ b/tests/runtime/test_cancel.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import time
+from typing import Any
+
+import pytest
+
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.runtime import get_runtime
+
+
+def iden(i: Any) -> Any:
+    return i
+
+
+def sleep1() -> None:
+    time.sleep(1)
+
+
+class TestCancelTask(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().submit(sleep1)
+        get_runtime().cancel(future)
+
+
+class TestCantAwaitCancelTask(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().submit(sleep1)
+        get_runtime().cancel(future)
+        await future
+
+
+def test_simple_cancel(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [TestCancelTask()])
+
+
+def test_cant_await_cancel(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    with pytest.raises(RuntimeError):
+        server_compiler.compile(circuit, [TestCantAwaitCancelTask()])
diff --git a/tests/runtime/test_execution.py b/tests/runtime/test_execution.py
new file mode 100644
index 000000000..0d44ba906
--- /dev/null
+++ b/tests/runtime/test_execution.py
@@ -0,0 +1,90 @@
+"""Checks the runtime's ability and correctness when executing tasks."""
+from __future__ import annotations
+
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.ir.gates import HGate
+from bqskit.ir.gates import XGate
+from bqskit.runtime import get_runtime
+
+
+def iden(i: int) -> int:
+    return i
+
+
+async def parent(i: int) -> tuple[int, int]:
+    return await get_runtime().map(iden, [2 * i, 2 * i + 1])
+
+
+async def add_hgate_to_circuit(c: Circuit) -> Circuit:
+    c.append_gate(HGate(), 0)
+    return c
+
+
+async def add_xgate_to_circuit(c: Circuit) -> Circuit:
+    c.append_gate(XGate(), 0)
+    return c
+
+
+class TestPass1(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        results = await get_runtime().map(iden, list(range(10)))
+        assert results == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+
+
+class TestPass2(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        results = await get_runtime().map(parent, list(range(1000)))
+        assert len(results) == 1000
+        assert all(r == [2 * i, 2 * i + 1] for i, r in enumerate(results))
+
+
+class TestPass3(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        out = await get_runtime().submit(add_hgate_to_circuit, circuit)
+        circuit.become(out)
+
+
+class TestPass4(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        out = await get_runtime().submit(add_xgate_to_circuit, circuit)
+        circuit.become(out)
+
+
+def test_simple_map(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [TestPass1()])
+
+
+def test_2level_map(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [TestPass2()])
+
+
+def test_circuit_change(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    out_circuit = server_compiler.compile(circuit, [TestPass3()])
+    assert HGate() in out_circuit.gate_set
+    assert out_circuit.num_operations == 1
+
+
+def test_reuse(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [TestPass1()])
+    server_compiler.compile(circuit, [TestPass1()])
+
+
+def test_parallel(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    t1 = server_compiler.submit(circuit, [TestPass3()])
+    t2 = server_compiler.submit(circuit, [TestPass4()])
+    c1 = server_compiler.result(t1)
+    c2 = server_compiler.result(t2)
+    assert isinstance(c1, Circuit)
+    assert isinstance(c2, Circuit)
+    assert HGate() in c1.gate_set
+    assert c1.num_operations == 1
+    assert XGate() in c2.gate_set
+    assert c2.num_operations == 1
diff --git a/tests/runtime/test_future.py b/tests/runtime/test_future.py
new file mode 100644
index 000000000..2d31bf6e1
--- /dev/null
+++ b/tests/runtime/test_future.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import time
+from typing import Any
+
+import pytest
+
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.runtime import get_runtime
+
+
+def iden(i: Any) -> Any:
+    return i
+
+
+def sleep1() -> None:
+    time.sleep(1)
+
+
+class TestPassFutureDone(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().submit(sleep1)
+        assert not future._done
+        await future
+        assert future._done
+
+
+class TestPassFutureCannotSend(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().submit(sleep1)
+        get_runtime().submit(iden, future)
+
+
+def test_future_done(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [TestPassFutureDone()])
+
+
+def test_future_cannot_send(server_compiler: Compiler) -> None:
+    circuit = Circuit(2)
+    with pytest.raises(RuntimeError):
+        server_compiler.compile(circuit, [TestPassFutureCannotSend()])
diff --git a/tests/runtime/test_logging.py b/tests/runtime/test_logging.py
new file mode 100644
index 000000000..a7f31584d
--- /dev/null
+++ b/tests/runtime/test_logging.py
@@ -0,0 +1,205 @@
+"""This module tests the logging features supported by BQSKit Runtime."""
+from __future__ import annotations
+
+import logging
+from io import StringIO
+
+import pytest
+
+from bqskit import enable_logging
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.runtime import get_runtime
+
+
+async def nested1() -> None:
+    logging.getLogger('bqskit').info('bqskit_level1')
+    logging.getLogger('dummy2').info('dummy2_level1')
+    await get_runtime().submit(nested2)
+
+
+async def nested2() -> None:
+    logging.getLogger('bqskit').info('bqskit_level2')
+    logging.getLogger('dummy2').info('dummy2_level2')
+    await get_runtime().submit(nested3)
+
+
+async def nested3() -> None:
+    logging.getLogger('bqskit').info('bqskit_level3')
+    logging.getLogger('dummy2').info('dummy2_level3')
+
+
+class TestInfoPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        logging.getLogger('bqskit').info('bqskit_info')
+        logging.getLogger('bqskit.dummy').info('bqskit_dummy_info')
+        logging.getLogger('dummy2').info('dummy2_info')
+
+
+class TestDebugPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        logging.getLogger('bqskit').debug('bqskit_debug')
+        logging.getLogger('bqskit.dummy').debug('bqskit_dummy_debug')
+        logging.getLogger('dummy2').debug('dummy2_debug')
+
+
+class TestNestedLogPass(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        logging.getLogger('bqskit').info('bqskit_level0')
+        logging.getLogger('dummy2').info('dummy2_level0')
+        await get_runtime().submit(nested1)
+
+
+def test_using_enable_logging(server_compiler: Compiler) -> None:
+    enable_logging()
+    logger = logging.getLogger('bqskit')
+    stream = StringIO()
+    handler = logging.StreamHandler(stream)
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    server_compiler.compile(Circuit(1), [TestInfoPass(), TestDebugPass()])
+    log = stream.getvalue()
+    assert 'bqskit_info' in log
+    assert 'bqskit_dummy_info' in log
+    assert 'dummy2_info' not in log
+    assert 'bqskit_debug' not in log
+    assert 'bqskit_dummy_debug' not in log
+    assert 'dummy2_debug' not in log
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+def test_using_enable_logging_verbose(server_compiler: Compiler) -> None:
+    enable_logging(True)
+    logger = logging.getLogger('bqskit')
+    handler = logging.StreamHandler(StringIO())
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    server_compiler.compile(Circuit(1), [TestInfoPass(), TestDebugPass()])
+    log = handler.stream.getvalue()
+    assert 'bqskit_info' in log
+    assert 'bqskit_dummy_info' in log
+    assert 'dummy2_info' not in log
+    assert 'bqskit_debug' in log
+    assert 'bqskit_dummy_debug' in log
+    assert 'dummy2_debug' not in log
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+def test_using_standard_logging(server_compiler: Compiler) -> None:
+    logger = logging.getLogger('bqskit')
+    logger.setLevel(logging.DEBUG)
+    handler = logging.StreamHandler(StringIO())
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    server_compiler.compile(Circuit(1), [TestInfoPass(), TestDebugPass()])
+    log = handler.stream.getvalue()
+    assert 'bqskit_info' in log
+    assert 'bqskit_dummy_info' in log
+    assert 'dummy2_info' not in log
+    assert 'bqskit_debug' in log
+    assert 'bqskit_dummy_debug' in log
+    assert 'dummy2_debug' not in log
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+def test_using_standard_logging_specific(server_compiler: Compiler) -> None:
+    logger = logging.getLogger('bqskit.dummy')
+    logger.setLevel(logging.DEBUG)
+    handler = logging.StreamHandler(StringIO())
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    server_compiler.compile(Circuit(1), [TestInfoPass(), TestDebugPass()])
+    log = handler.stream.getvalue()
+    assert 'bqskit_info' not in log
+    assert 'bqskit_dummy_info' in log
+    assert 'dummy2_info' not in log
+    assert 'bqskit_debug' not in log
+    assert 'bqskit_dummy_debug' in log
+    assert 'dummy2_debug' not in log
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+def test_using_external_logging(server_compiler: Compiler) -> None:
+    logger = logging.getLogger('dummy2')
+    logger.setLevel(logging.DEBUG)
+    handler = logging.StreamHandler(StringIO())
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+    server_compiler.compile(Circuit(1), [TestInfoPass(), TestDebugPass()])
+    log = handler.stream.getvalue()
+    assert 'bqskit_info' not in log
+    assert 'bqskit_dummy_info' not in log
+    assert 'dummy2_info' in log
+    assert 'bqskit_debug' not in log
+    assert 'bqskit_dummy_debug' not in log
+    assert 'dummy2_debug' in log
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+@pytest.mark.parametrize('level', [-1, 0, 1, 2, 3, 4])
+def test_limiting_nested_calls_enable_logging(
+    server_compiler: Compiler,
+    level: int,
+) -> None:
+    enable_logging()
+    logger = logging.getLogger('bqskit')
+    stream = StringIO()
+    handler = logging.StreamHandler(stream)
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+
+    server_compiler.compile(
+        Circuit(1),
+        [TestNestedLogPass()],
+        max_logging_depth=level,
+    )
+
+    log = stream.getvalue()
+    if level != -1:
+        expected_prints = list(range(0, min(4, level + 1)))
+    else:
+        expected_prints = [0, 1, 2, 3]
+    unexpected_prints = [x for x in [0, 1, 2, 3] if x not in expected_prints]
+    assert all(f'bqskit_level{i}' in log for i in expected_prints)
+    assert all(f'bqskit_level{i}' not in log for i in unexpected_prints)
+    assert all(f'dummy2_level{i}' not in log for i in [0, 1, 2, 3])
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
+
+
+@pytest.mark.parametrize('level', [-1, 0, 1, 2, 3, 4])
+def test_limiting_nested_calls_external_logging(
+    server_compiler: Compiler,
+    level: int,
+) -> None:
+    logger = logging.getLogger('dummy2')
+    logger.setLevel(logging.INFO)
+    stream = StringIO()
+    handler = logging.StreamHandler(stream)
+    handler.setLevel(logging.DEBUG)
+    logger.addHandler(handler)
+
+    server_compiler.compile(
+        Circuit(1),
+        [TestNestedLogPass()],
+        max_logging_depth=level,
+    )
+
+    log = stream.getvalue()
+    if level != -1:
+        expected_prints = list(range(0, min(4, level + 1)))
+    else:
+        expected_prints = [0, 1, 2, 3]
+    unexpected_prints = [x for x in [0, 1, 2, 3] if x not in expected_prints]
+    assert all(f'dummy2_level{i}' in log for i in expected_prints)
+    assert all(f'dummy2_level{i}' not in log for i in unexpected_prints)
+    assert all(f'bqskit_level{i}' not in log for i in [0, 1, 2, 3])
+    logger.removeHandler(handler)
+    logger.setLevel(logging.WARNING)
diff --git a/tests/runtime/test_next.py b/tests/runtime/test_next.py
new file mode 100644
index 000000000..254f65064
--- /dev/null
+++ b/tests/runtime/test_next.py
@@ -0,0 +1,83 @@
+"""This module tests the runtime's ability to wake tasks on first result."""
+from __future__ import annotations
+
+import time
+
+import pytest
+
+from bqskit.compiler import BasePass
+from bqskit.compiler import Compiler
+from bqskit.compiler.passdata import PassData
+from bqskit.ir import Circuit
+from bqskit.runtime import get_runtime
+
+
+def sleepi(i: int) -> int:
+    time.sleep(i)
+    return i
+
+
+class TestNoDuplicateResult(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().map(sleepi, [0.3, 0.4, 0.1, 0.2])
+        int_ids = await get_runtime().next(future)
+        seen = [0]
+        for int_id in int_ids:
+            assert not int_id[1] in seen
+            seen.append(int_id[1])
+
+
+class TestNoDuplicateResultsInTwoNexts(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().map(sleepi, [0.3, 0.4, 0.1, 0.2])
+        seen = [0]
+        int_ids = await get_runtime().next(future)
+
+        for int_id in int_ids:
+            assert not int_id[1] in seen
+            seen.append(int_id[1])
+
+        int_ids = await get_runtime().next(future)
+
+        for int_id in int_ids:
+            assert not int_id[1] in seen
+            seen.append(int_id[1])
+
+
+class TestFutureAwaitGivesAllResultsAfterNext(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        sleep_times = [0.3, 0.4, 0.1, 0.2]
+        future = get_runtime().map(sleepi, sleep_times)
+        _ = await get_runtime().next(future)
+        assert (await future) == sleep_times
+
+
+class TestNextOnCompleteTaskRaisesError(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().map(sleepi, [0.3, 0.4, 0.1, 0.2])
+        _ = await future
+        with pytest.raises(RuntimeError):
+            _ = await get_runtime().next(future)
+
+
+class TestNextNeverlosesResults(BasePass):
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        future = get_runtime().map(sleepi, [0.1])
+        time.sleep(0.2)
+        int_ids = await get_runtime().next(future)
+        assert len(int_ids) == 1  # Only one should have been received
+        assert int_ids[0] == (0, .1)  # the 0th index 1 element
+
+
+@pytest.mark.parametrize(
+    'test_pass', [
+        TestNoDuplicateResult(),
+        TestNoDuplicateResultsInTwoNexts(),
+        TestFutureAwaitGivesAllResultsAfterNext(),
+        TestNextOnCompleteTaskRaisesError(),
+        TestNextNeverlosesResults(),
+    ],
+)
+def test_next_pass(server_compiler: Compiler, test_pass: BasePass) -> None:
+    circuit = Circuit(2)
+    server_compiler.compile(circuit, [test_pass])
diff --git a/tests/runtime/test_task.py b/tests/runtime/test_task.py
new file mode 100644
index 000000000..0bcb1aa48
--- /dev/null
+++ b/tests/runtime/test_task.py
@@ -0,0 +1,78 @@
+"""This module tests RuntimeTask functionality."""
+from __future__ import annotations
+
+from typing import Any
+from typing import Awaitable
+from typing import Generator
+
+import pytest
+
+from bqskit.runtime.address import RuntimeAddress
+from bqskit.runtime.task import RuntimeTask
+
+
+class AwaitableTestObject(Awaitable[Any]):
+    def __await__(self) -> Generator[str, str | None, str]:
+        yield 'test1'
+        assert (yield 'test2') == 'test_send'
+        assert (yield 'test3') is None
+        assert (yield 'test4') is None
+        return 'test_return'
+
+
+async def coro_test() -> str:
+    assert (await AwaitableTestObject()) == 'test_return'
+    return 'test_complete'
+
+
+def test_uninit_task_step_errors() -> None:
+    addr = RuntimeAddress(0, 0, 0)
+    with pytest.raises(RuntimeError):
+        task = RuntimeTask((coro_test, tuple(), {}), addr, 0, tuple())
+        task.step()
+
+
+def test_task_step() -> None:
+    addr = RuntimeAddress(0, 0, 0)
+    task = RuntimeTask((coro_test, tuple(), {}), addr, 0, tuple())
+    task.start()
+    assert task.step() == 'test1'
+    assert task.step() == 'test2'
+    assert task.step('test_send') == 'test3'
+    assert task.step() == 'test4'
+
+    try:
+        task.step()
+    except StopIteration as e:
+        assert e.value == 'test_complete'
+    else:
+        assert False, 'StopIteration next called.'
+
+
+def not_coro_test() -> str:
+    return 'test_complete'
+
+
+def test_task_step_no_coro() -> None:
+    addr = RuntimeAddress(0, 0, 0)
+    task = RuntimeTask((not_coro_test, tuple(), {}), addr, 0, tuple())
+    task.start()
+    try:
+        task.step()
+    except StopIteration as e:
+        assert e.value == 'test_complete'
+    else:
+        assert False, 'StopIteration next called.'
+
+
+def test_task_descendant_of() -> None:
+    task = RuntimeTask(
+        (coro_test, tuple(), {}),
+        RuntimeAddress(0, 0, 0),
+        0,
+        (RuntimeAddress(0, 0, 1), RuntimeAddress(0, 0, 2)),
+    )
+    assert task.is_descendant_of(RuntimeAddress(0, 0, 0))
+    assert task.is_descendant_of(RuntimeAddress(0, 0, 1))
+    assert task.is_descendant_of(RuntimeAddress(0, 0, 2))
+    assert not task.is_descendant_of(RuntimeAddress(1, 0, 0))