diff --git a/src/integrations/prefect-dask/prefect_dask/task_runners.py b/src/integrations/prefect-dask/prefect_dask/task_runners.py
index f4edbab25295..60066faaf870 100644
--- a/src/integrations/prefect-dask/prefect_dask/task_runners.py
+++ b/src/integrations/prefect-dask/prefect_dask/task_runners.py
@@ -319,7 +319,7 @@ def submit(
         self,
         task: "Task[P, Coroutine[Any, Any, R]]",
         parameters: Dict[str, Any],
-        wait_for: Optional[Iterable[PrefectFuture]] = None,
+        wait_for: Optional[Iterable[PrefectDaskFuture[R]]] = None,
         dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
     ) -> PrefectDaskFuture[R]:
         ...
@@ -329,26 +329,26 @@ def submit(
         self,
         task: "Task[Any, R]",
         parameters: Dict[str, Any],
-        wait_for: Optional[Iterable[PrefectFuture]] = None,
+        wait_for: Optional[Iterable[PrefectDaskFuture[R]]] = None,
         dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
     ) -> PrefectDaskFuture[R]:
         ...

     def submit(
         self,
-        task: Task,
+        task: "Union[Task[P, R], Task[P, Coroutine[Any, Any, R]]]",
         parameters: Dict[str, Any],
-        wait_for: Optional[Iterable[PrefectFuture]] = None,
+        wait_for: Optional[Iterable[PrefectDaskFuture[R]]] = None,
         dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
-    ) -> PrefectDaskFuture:
+    ) -> PrefectDaskFuture[R]:
         if not self._started:
             raise RuntimeError(
                 "The task runner must be started before submitting work."
             )

-        # unpack the upstream call in order to cast Prefect futures to Dask futures
-        # where possible to optimize Dask task scheduling
+        # Convert both parameters and wait_for futures to Dask futures
         parameters = self._optimize_futures(parameters)
+        wait_for = self._optimize_futures(wait_for) if wait_for else None

         future = self._client.submit(
             task,
@@ -357,7 +357,9 @@ def submit(
             dependencies=dependencies,
             return_type="state",
         )
-        return PrefectDaskFuture(wrapped_future=future, task_run_id=future.task_run_id)
+        return PrefectDaskFuture[R](
+            wrapped_future=future, task_run_id=future.task_run_id
+        )

     @overload
     def map(

diff --git a/src/integrations/prefect-dask/tests/test_task_runners.py b/src/integrations/prefect-dask/tests/test_task_runners.py
index 748de299c65e..d1bc5c8bec0b 100644
--- a/src/integrations/prefect-dask/tests/test_task_runners.py
+++ b/src/integrations/prefect-dask/tests/test_task_runners.py
@@ -452,3 +452,49 @@ def umbrella_flow():
             return future.result()

         assert umbrella_flow() == "nested task"
+
+    def test_state_dependencies_via_wait_for(self, task_runner):
+        @task
+        def task_a():
+            return time.time()
+
+        @task
+        def task_b():
+            return time.time()
+
+        @flow(task_runner=task_runner)
+        def test_flow() -> tuple[float, float]:
+            a = task_a.submit()
+            b = task_b.submit(wait_for=[a])
+            return a.result(), b.result()
+
+        a_time, b_time = test_flow()
+
+        assert b_time > a_time, "task_b timestamp should be after task_a timestamp"
+
+    def test_state_dependencies_via_wait_for_disparate_upstream_tasks(
+        self, task_runner
+    ):
+        @task
+        def task_a():
+            return time.time()
+
+        @task
+        def task_b():
+            return time.time()
+
+        @task
+        def task_c():
+            return time.time()
+
+        @flow(task_runner=task_runner)
+        def test_flow() -> tuple[float, float, float]:
+            a = task_a.submit()
+            b = task_b.submit()
+            c = task_c.submit(wait_for=[a, b])
+
+            return a.result(), b.result(), c.result()
+
+        a_time, b_time, c_time = test_flow()
+
+        assert c_time > a_time and c_time > b_time

DaskTaskRunner can not work together with `wait_for`
### Bug summary

When using the `DaskTaskRunner` using wait_for is broken. Using the `ThreadPoolTaskRunner` the same flow works just fine.

```py
from prefect import flow, task
from prefect.task_runners import ThreadPoolTaskRunner
from prefect_dask.task_runners import DaskTaskRunner

@task()
def task_a() -> bool:
    print("Task A")
    return True

@task()
def task_b() -> bool:

    print("Task B")
    return True

@flow(name='test_flow', log_prints=True, task_runner=DaskTaskRunner()) #ThreadPoolTaskRunner()
def main_flow():
    task_a_future = task_a.submit()
    task_b_future = task_b.submit(wait_for=[task_a_future])

    task_b_future.wait()

if __name__ == '__main__':
    main_flow()
```

generates the errors:

```
Exception ignored in: <function PrefectDaskFuture.__del__ at 0x00000199CFD7B4C0>
Traceback (most recent call last):
  File "C:\mambaforge\envs\prefect\Lib\site-packages\prefect_dask\task_runners.py", line 153, in __del__
    if self._final_state or self._wrapped_future.done():
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\mambaforge\envs\prefect\Lib\site-packages\distributed\client.py", line 378, in done
    return self._state.done()
           ^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'done'
Exception ignored in: <function PrefectDaskFuture.__del__ at 0x00000199CFD7B4C0>
Traceback (most recent call last):
  File "C:\mambaforge\envs\prefect\Lib\site-packages\prefect_dask\task_runners.py", line 153, in __del__
    if self._final_state or self._wrapped_future.done():
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\mambaforge\envs\prefect\Lib\site-packages\distributed\client.py", line 378, in done
    return self._state.done()
           ^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'done'
Exception ignored in: <function PrefectDaskFuture.__del__ at 0x00000199CFD7B4C0>
Traceback (most recent call last):
  File "C:\mambaforge\envs\prefect\Lib\site-packages\prefect_dask\task_runners.py", line 153, in __del__
    if self._final_state or self._wrapped_future.done():
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\mambaforge\envs\prefect\Lib\site-packages\distributed\client.py", line 378, in done
    return self._state.done()
           ^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'done'
```

Seems like wrapping the normal future is broken when wait_for is used. If we use the return value of task_a in task_b and skip the wait_for the behaviour under both TaskRunners is correct.

### Version info

```Text
Version:             3.0.10
API version:         0.8.4
Python version:      3.12.7
Git commit:          3aa2d893
Built:               Tue, Oct 15, 2024 1:31 PM
OS/Arch:             win32/AMD64
Profile:             ephemeral
Server type:         server
Pydantic version:    2.9.2
Integrations:
  prefect-dask:      0.3.1
```

### Additional context

_No response_

hi @EraYaN - thank you for the issue! will take a look at this


diff --git a/src/prefect/cli/deploy.py b/src/prefect/cli/deploy.py
index cf15c23ab6ee..cfa321482e55 100644
--- a/src/prefect/cli/deploy.py
+++ b/src/prefect/cli/deploy.py
@@ -41,6 +41,7 @@
 )
 from prefect.cli.root import app, is_interactive
 from prefect.client.schemas.actions import DeploymentScheduleCreate
+from prefect.client.schemas.objects import ConcurrencyLimitConfig
 from prefect.client.schemas.schedules import (
     CronSchedule,
     IntervalSchedule,
@@ -242,6 +243,11 @@ async def deploy(
         "--concurrency-limit",
         help=("The maximum number of concurrent runs for this deployment."),
     ),
+    concurrency_limit_collision_strategy: str = typer.Option(
+        None,
+        "--collision-strategy",
+        help="Configure the behavior for runs once the concurrency limit is reached. Falls back to `ENQUEUE` if unset.",
+    ),
     work_pool_name: str = SettingsOption(
         PREFECT_DEFAULT_WORK_POOL_NAME,
         "-p",
@@ -371,12 +377,23 @@ async def deploy(
         job_variables = list()
     job_variables.extend(variables)

+    concurrency_limit_config = (
+        None
+        if concurrency_limit is None
+        else concurrency_limit
+        if concurrency_limit_collision_strategy is None
+        else ConcurrencyLimitConfig(
+            limit=concurrency_limit,
+            collision_strategy=concurrency_limit_collision_strategy,
+        ).model_dump()
+    )
+
     options = {
         "entrypoint": entrypoint,
         "description": description,
         "version": version,
         "tags": tags,
-        "concurrency_limit": concurrency_limit,
+        "concurrency_limit": concurrency_limit_config,
         "work_pool_name": work_pool_name,
         "work_queue_name": work_queue_name,
         "variables": job_variables,

diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py
index 661494a4ce4a..1f455b62306f 100644
--- a/tests/cli/test_deploy.py
+++ b/tests/cli/test_deploy.py
@@ -276,7 +276,7 @@ async def test_project_deploy(self, project_dir, prefect_client: PrefectClient):
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command=(
-                "deploy ./flows/hello.py:my_flow -n test-name -p test-pool -cl 42 --version"
+                "deploy ./flows/hello.py:my_flow -n test-name -p test-pool --version"
                 " 1.0.0 -v env=prod -t foo-bar"
             ),
             expected_code=0,
@@ -293,7 +293,6 @@ async def test_project_deploy(self, project_dir, prefect_client: PrefectClient):
         assert deployment.work_pool_name == "test-pool"
         assert deployment.version == "1.0.0"
         assert deployment.tags == ["foo-bar"]
-        assert deployment.global_concurrency_limit.limit == 42
         assert deployment.job_variables == {"env": "prod"}
         assert deployment.enforce_parameter_schema

@@ -446,6 +445,69 @@ async def test_deploy_does_not_prompt_storage_when_pull_step_exists(
             ],
         )

+    @pytest.mark.parametrize(
+        "cli_options,expected_limit,expected_strategy",
+        [
+            pytest.param("-cl 42", 42, None, id="limit-only"),
+            pytest.param(
+                "-cl 42 --collision-strategy CANCEL_NEW",
+                42,
+                "CANCEL_NEW",
+                id="limit-and-strategy",
+            ),
+            pytest.param(
+                "--collision-strategy CANCEL_NEW",
+                None,
+                None,
+                id="strategy-only",
+            ),
+        ],
+    )
+    async def test_deploy_with_concurrency_limit_and_options(
+        self,
+        project_dir,
+        prefect_client: PrefectClient,
+        cli_options,
+        expected_limit,
+        expected_strategy,
+    ):
+        await prefect_client.create_work_pool(
+            WorkPoolCreate(name="test-pool", type="test")
+        )
+        await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-deploy-concurrency-limit -p test-pool "
+                + cli_options
+                # "-cl 42 --collision-strategy CANCEL_NEW"
+            ),
+            expected_code=0,
+            expected_output_contains=[
+                "An important name/test-deploy-concurrency-limit",
+                "prefect worker start --pool 'test-pool'",
+            ],
+        )
+
+        deployment = await prefect_client.read_deployment_by_name(
+            "An important name/test-deploy-concurrency-limit"
+        )
+        assert deployment.name == "test-deploy-concurrency-limit"
+        assert deployment.work_pool_name == "test-pool"
+
+        if expected_limit is not None:
+            assert deployment.global_concurrency_limit is not None
+            assert deployment.global_concurrency_limit.limit == expected_limit
+        else:
+            assert deployment.global_concurrency_limit is None
+
+        if expected_strategy is not None:
+            assert deployment.concurrency_options is not None
+            assert (
+                deployment.concurrency_options.collision_strategy == expected_strategy
+            )
+        else:
+            assert deployment.concurrency_options is None
+
     class TestGeneratedPullAction:
         async def test_project_deploy_generates_pull_action(
             self, work_pool, prefect_client, uninitialized_project_dir

Add ability to configure `ConcurrenyOptions` to the CLI
### Describe the current behavior

Currently users can set the the concurrency_limit for a deployment when creating it with the CLI by specifying the limit with and int as so: `deploy "my-deployment -cl 1".

Upcoming work #15291, adds options for handling concurrency limiting which we will need add to the CLI.

### Describe the proposed behavior

I think that it while it's possible to create a custom type in typer and configure them as so `deploy "my-deployment" -cl 1 --concurrency-options = {'collisions-strategy' = 'cancel-new'}`, it makes for a better user experience if we flatten it. This could be done for each subsequent options that gets added and along with sane defaults, users wouldn't be setting a lot of these.

### Example Use

`deploy "my-deployment" -cl 42 --collision-strategy "cancel-new"`

### Additional context

_No response_


diff --git a/src/integrations/prefect-docker/prefect_docker/worker.py b/src/integrations/prefect-docker/prefect_docker/worker.py
index bd85f4525325..9ebba4986f16 100644
--- a/src/integrations/prefect-docker/prefect_docker/worker.py
+++ b/src/integrations/prefect-docker/prefect_docker/worker.py
@@ -101,6 +101,7 @@ class DockerWorkerJobConfiguration(BaseJobConfiguration):
             `mem_limit` is 300m and `memswap_limit` is not set, containers can use
             600m in total of memory and swap.
         privileged: Give extended privileges to created containers.
+        container_create_kwargs: Extra args for docker py when creating container.
     """

     image: str = Field(
@@ -165,11 +166,17 @@ class DockerWorkerJobConfiguration(BaseJobConfiguration):
             "600m in total of memory and swap."
         ),
     )
-
     privileged: bool = Field(
         default=False,
         description="Give extended privileges to created container.",
     )
+    container_create_kwargs: Optional[Dict[str, Any]] = Field(
+        default=None,
+        title="Container Configuration",
+        description=(
+            "Configuration for containers created by workers. See the [`docker-py` documentation](https://docker-py.readthedocs.io/en/stable/containers.html) for accepted values."
+        ),
+    )

     @validator("volumes")
     def _validate_volume_format(cls, volumes):
@@ -526,6 +533,18 @@ def _build_container_settings(
     ) -> Dict:
         """Builds a dictionary of container settings to pass to the Docker API."""
         network_mode = configuration.get_network_mode()
+
+        container_create_kwargs = (
+            configuration.container_create_kwargs
+            if configuration.container_create_kwargs
+            else {}
+        )
+        container_create_kwargs = {
+            k: v
+            for k, v in container_create_kwargs.items()
+            if k not in configuration.__fields__
+        }
+
         return dict(
             image=configuration.image,
             network=configuration.networks[0] if configuration.networks else None,
@@ -540,6 +559,7 @@ def _build_container_settings(
             mem_limit=configuration.mem_limit,
             memswap_limit=configuration.memswap_limit,
             privileged=configuration.privileged,
+            **container_create_kwargs,
         )

     def _create_and_start_container(

diff --git a/src/integrations/prefect-docker/tests/test_worker.py b/src/integrations/prefect-docker/tests/test_worker.py
index 1fa081ccd3b4..55130027f4af 100644
--- a/src/integrations/prefect-docker/tests/test_worker.py
+++ b/src/integrations/prefect-docker/tests/test_worker.py
@@ -850,6 +850,37 @@ async def test_task_infra_pid_includes_host_and_container_id(
     assert result.identifier == f"{FAKE_BASE_URL}:{FAKE_CONTAINER_ID}"

+async def test_container_create_kwargs(
+    mock_docker_client, flow_run, default_docker_worker_job_configuration
+):
+    default_docker_worker_job_configuration.container_create_kwargs = {
+        "hostname": "custom_name"
+    }
+    async with DockerWorker(work_pool_name="test") as worker:
+        await worker.run(
+            flow_run=flow_run, configuration=default_docker_worker_job_configuration
+        )
+    mock_docker_client.containers.create.assert_called_once()
+    hostname = mock_docker_client.containers.create.call_args[1].get("hostname")
+    assert hostname == "custom_name"
+
+
+async def test_container_create_kwargs_excludes_job_variables(
+    mock_docker_client, flow_run, default_docker_worker_job_configuration
+):
+    default_docker_worker_job_configuration.name = "job_config_name"
+    default_docker_worker_job_configuration.container_create_kwargs = {
+        "name": "create_kwarg_name"
+    }
+    async with DockerWorker(work_pool_name="test") as worker:
+        await worker.run(
+            flow_run=flow_run, configuration=default_docker_worker_job_configuration
+        )
+    mock_docker_client.containers.create.assert_called_once()
+    name = mock_docker_client.containers.create.call_args[1].get("name")
+    assert name == "job_config_name"
+
+
 async def test_task_status_receives_result_identifier(
     mock_docker_client, flow_run, default_docker_worker_job_configuration
 ):

Configure `ipc_mode` or `shm_size` on container creation
For some flows that utilize `multiprocessing` for ML workloads, I need to run containers with increased shared memory available under `/dev/shm` (`--shm_size`) or use the host's namespace (--ipc_mode='host').  Looking through the code, I believe this would expand `DockerWorkerJobConfiguration` to include these two settings, which are available in `Container.create`.

Proposed additions:
- Add `ipc_mode` and `shm_size` to `DockerWorkerJobConfiguration`, both defaulting to `None` to preserve default behavior.

@desertaxle

Thanks for the issue @geoffrp! I think this is a great opportunity to expose more configurability for the Docker worker so that we don't need to update the code each time a new parameter is needed.
I would tend to agree, lots of arguments to play with for custom containers.  Are you suggesting removing `DockerWorkerJobConfiguration`?  I'm assuming that this construct allows the UI to know what fields may be specified if not creating through the command line?
We'll need to expand `DockerWorkerJobConfiguration` to include a field that accepts an arbitrary dictionary of attributes passed directly to `docker-py` functions. This will allow users to modify the base job template of their work pools to suit their needs without requiring additional code changes to the `DockerWorker`.
@desertaxle Is there a prospective timeline for this feature?  I would be willing to assist if possible


diff --git a/src/prefect/futures.py b/src/prefect/futures.py
index d0df04ea9a5a..7379aa8c8b06 100644
--- a/src/prefect/futures.py
+++ b/src/prefect/futures.py
@@ -1,4 +1,5 @@
 import abc
+import collections
 import concurrent.futures
 import inspect
 import uuid
@@ -256,13 +257,7 @@ def wait(self, timeout: Optional[float] = None) -> None:
             timeout: The maximum number of seconds to wait for all futures to
                 complete. This method will not raise if the timeout is reached.
         """
-        try:
-            with timeout_context(timeout):
-                for future in self:
-                    future.wait()
-        except TimeoutError:
-            logger.debug("Timed out waiting for all futures to complete.")
-            return
+        wait(self, timeout=timeout)

     def result(
         self,
@@ -297,6 +292,57 @@ def result(
             ) from exc

+DoneAndNotDoneFutures = collections.namedtuple("DoneAndNotDoneFutures", "done not_done")
+
+
+def wait(futures: List[PrefectFuture], timeout=None) -> DoneAndNotDoneFutures:
+    """
+    Wait for the futures in the given sequence to complete.
+
+    Args:
+        futures: The sequence of Futures to wait upon.
+        timeout: The maximum number of seconds to wait. If None, then there
+            is no limit on the wait time.
+
+    Returns:
+        A named 2-tuple of sets. The first set, named 'done', contains the
+        futures that completed (is finished or cancelled) before the wait
+        completed. The second set, named 'not_done', contains uncompleted
+        futures. Duplicate futures given to *futures* are removed and will be
+        returned only once.
+
+    Examples:
+        ```python
+        @task
+        def sleep_task(seconds):
+            sleep(seconds)
+            return 42
+
+        @flow
+        def flow():
+            futures = random_task.map(range(10))
+            done, not_done = wait(futures, timeout=5)
+            print(f"Done: {len(done)}")
+            print(f"Not Done: {len(not_done)}")
+        ```
+    """
+    futures = set(futures)
+    done = {f for f in futures if f._final_state}
+    not_done = futures - done
+    if len(done) == len(futures):
+        return DoneAndNotDoneFutures(done, not_done)
+    try:
+        with timeout_context(timeout):
+            for future in not_done.copy():
+                future.wait()
+                done.add(future)
+                not_done.remove(future)
+            return DoneAndNotDoneFutures(done, not_done)
+    except TimeoutError:
+        logger.debug("Timed out waiting for all futures to complete.")
+        return DoneAndNotDoneFutures(done, not_done)
+
+
 def resolve_futures_to_states(
     expr: Union[PrefectFuture, Any],
 ) -> Union[State, Any]:

diff --git a/tests/test_futures.py b/tests/test_futures.py
index e009e0fa1716..43fcd2dc2460 100644
--- a/tests/test_futures.py
+++ b/tests/test_futures.py
@@ -16,6 +16,7 @@
     PrefectFutureList,
     PrefectWrappedFuture,
     resolve_futures_to_states,
+    wait,
 )
 from prefect.states import Completed, Failed
 from prefect.task_engine import run_task_async, run_task_sync
@@ -37,6 +38,24 @@ def result(
         return self._final_state.result()

+class TestUtilityFunctions:
+    def test_wait(self):
+        mock_futures = [MockFuture(data=i) for i in range(5)]
+        futures = wait(mock_futures)
+        assert futures.not_done == set()
+
+        for future in mock_futures:
+            assert future.state.is_completed()
+
+    @pytest.mark.timeout(method="thread")
+    def test_wait_with_timeout(self):
+        mock_futures = [MockFuture(data=i) for i in range(5)]
+        hanging_future = Future()
+        mock_futures.append(PrefectConcurrentFuture(uuid.uuid4(), hanging_future))
+        futures = wait(mock_futures, timeout=0.01)
+        assert futures.not_done == {mock_futures[-1]}
+
+
 class TestPrefectConcurrentFuture:
     def test_wait_with_timeout(self):
         wrapped_future = Future()

Add `wait` utility for `PrefectFuture`s
Add a `wait` utility that mirrors `concurrent.futures.wait`. Should accept an iterable of futures to wait for completion and a timeout. It should return a tuple of futures that have completed and those that have not yet completed.


diff --git a/src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py b/src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py
index bdda680e3772..f98b109fb371 100644
--- a/src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py
+++ b/src/integrations/prefect-sqlalchemy/prefect_sqlalchemy/database.py
@@ -609,7 +609,7 @@ async def execute(
         operation: str,
         parameters: Optional[Dict[str, Any]] = None,
         **execution_options: Dict[str, Any],
-    ) -> None:
+    ) -> CursorResult:
         """
         Executes an operation on the database. This method is intended to be used
         for operations that do not return data, such as INSERT, UPDATE, or DELETE.
@@ -636,13 +636,14 @@ async def execute(
             ```
         """  # noqa
         async with self._manage_connection(begin=False) as connection:
-            await self._async_sync_execute(
+            result = await self._async_sync_execute(
                 connection,
                 text(operation),
                 parameters,
                 execution_options=execution_options,
             )
         self.logger.info(f"Executed the operation, {operation!r}")
+        return result

     @sync_compatible
     async def execute_many(
@@ -650,7 +651,7 @@ async def execute_many(
         operation: str,
         seq_of_parameters: List[Dict[str, Any]],
         **execution_options: Dict[str, Any],
-    ) -> None:
+    ) -> CursorResult:
         """
         Executes many operations on the database. This method is intended to be used
         for operations that do not return data, such as INSERT, UPDATE, or DELETE.
@@ -681,7 +682,7 @@ async def execute_many(
             ```
         """  # noqa
         async with self._manage_connection(begin=False) as connection:
-            await self._async_sync_execute(
+            result = await self._async_sync_execute(
                 connection,
                 text(operation),
                 seq_of_parameters,
@@ -690,6 +691,7 @@ async def execute_many(
         self.logger.info(
             f"Executed {len(seq_of_parameters)} operations based off {operation!r}."
         )
+        return result

     async def __aenter__(self):
         """

diff --git a/src/integrations/prefect-sqlalchemy/tests/test_database.py b/src/integrations/prefect-sqlalchemy/tests/test_database.py
index bd17537ea2c2..a2f0327ace48 100644
--- a/src/integrations/prefect-sqlalchemy/tests/test_database.py
+++ b/src/integrations/prefect-sqlalchemy/tests/test_database.py
@@ -12,6 +12,7 @@
 )
 from sqlalchemy import __version__ as SQLALCHEMY_VERSION
 from sqlalchemy.engine import Connection, Engine
+from sqlalchemy.engine.cursor import CursorResult
 from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine

 from prefect import flow, task
@@ -156,14 +157,14 @@ async def connector_with_data(self, tmp_path, request):
             ),
             fetch_size=2,
         )
-        await credentials.execute(
+        create_result = await credentials.execute(
             "CREATE TABLE IF NOT EXISTS customers (name varchar, address varchar);"
         )
-        await credentials.execute(
+        insert_result = await credentials.execute(
             "INSERT INTO customers (name, address) VALUES (:name, :address);",
             parameters={"name": "Marvin", "address": "Highway 42"},
         )
-        await credentials.execute_many(
+        many_result = await credentials.execute_many(
             "INSERT INTO customers (name, address) VALUES (:name, :address);",
             seq_of_parameters=[
                 {"name": "Ford", "address": "Highway 42"},
@@ -171,6 +172,9 @@ async def connector_with_data(self, tmp_path, request):
                 {"name": "Me", "address": "Myway 88"},
             ],
         )
+        assert isinstance(many_result, CursorResult)
+        assert isinstance(insert_result, CursorResult)
+        assert isinstance(create_result, CursorResult)
         yield credentials

     @pytest.fixture(params=[True, False])

return `CursorResult` from prefect_sqlalchemy execute/execute_many methods
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

3.x

### Describe the current behavior

`execute_many` method returns `None`

### Describe the proposed behavior

`execute_many` method would return `CursorResult` so that users could interact with the result of the executed query

### Example Use

_No response_

### Additional context

_No response_


diff --git a/src/prefect/tasks.py b/src/prefect/tasks.py
index ec2dcd497d0b..ece4963f9dc0 100644
--- a/src/prefect/tasks.py
+++ b/src/prefect/tasks.py
@@ -849,7 +849,11 @@ def submit(
         flow_run_context = FlowRunContext.get()

         if not flow_run_context:
-            raise ValueError("Task.submit() must be called within a flow")
+            raise RuntimeError(
+                "Unable to determine task runner to use for submission. If you are"
+                " submitting a task outside of a flow, please use `.delay`"
+                " to submit the task run for deferred execution."
+            )

         task_viz_tracker = get_task_viz_tracker()
         if task_viz_tracker:
@@ -897,6 +901,7 @@ def map(
         *args: Any,
         return_state: bool = False,
         wait_for: Optional[Iterable[PrefectFuture]] = None,
+        deferred: bool = False,
         **kwargs: Any,
     ):
         """
@@ -1010,6 +1015,7 @@ def map(
             [[11, 21], [12, 22], [13, 23]]
         """

+        from prefect.task_runners import TaskRunner
         from prefect.utilities.visualization import (
             VisualizationUnsupportedError,
             get_task_viz_tracker,
@@ -1026,22 +1032,22 @@ def map(
                 "`task.map()` is not currently supported by `flow.visualize()`"
             )

-        if not flow_run_context:
-            # TODO: Should we split out background task mapping into a separate method
-            # like we do for the `submit`/`apply_async` split?
+        if deferred:
             parameters_list = expand_mapping_parameters(self.fn, parameters)
-            # TODO: Make this non-blocking once we can return a list of futures
-            # instead of a list of task runs
-            return [
-                run_coro_as_sync(self.create_run(parameters=parameters, deferred=True))
+            futures = [
+                self.apply_async(kwargs=parameters, wait_for=wait_for)
                 for parameters in parameters_list
             ]
-
-        from prefect.task_runners import TaskRunner
-
-        task_runner = flow_run_context.task_runner
-        assert isinstance(task_runner, TaskRunner)
-        futures = task_runner.map(self, parameters, wait_for)
+        elif task_runner := getattr(flow_run_context, "task_runner", None):
+            assert isinstance(task_runner, TaskRunner)
+            futures = task_runner.map(self, parameters, wait_for)
+        else:
+            raise RuntimeError(
+                "Unable to determine task runner to use for mapped task runs. If"
+                " you are mapping a task outside of a flow, please provide"
+                " `deferred=True` to submit the mapped task runs for deferred"
+                " execution."
+            )
         if return_state:
             states = []
             for future in futures:

diff --git a/tests/test_background_tasks.py b/tests/test_background_tasks.py
index 40a9e34c3015..9dd3a70047d8 100644
--- a/tests/test_background_tasks.py
+++ b/tests/test_background_tasks.py
@@ -333,7 +333,7 @@ async def test_call_with_return_state(self, async_foo_task):

 class TestMap:
     async def test_map(self, async_foo_task):
-        task_runs = async_foo_task.map([1, 2, 3])
+        task_runs = async_foo_task.map([1, 2, 3], deferred=True)

         assert len(task_runs) == 3

@@ -350,7 +350,7 @@ async def test_map_with_implicitly_unmapped_kwargs(self):
         def bar(x: int, unmappable: int) -> Tuple[int, int]:
             return (x, unmappable)

-        task_runs = bar.map([1, 2, 3], unmappable=42)
+        task_runs = bar.map([1, 2, 3], unmappable=42, deferred=True)

         assert len(task_runs) == 3

@@ -367,7 +367,7 @@ async def test_async_map_with_implicitly_unmapped_kwargs(self):
         async def bar(x: int, unmappable: int) -> Tuple[int, int]:
             return (x, unmappable)

-        task_runs = bar.map([1, 2, 3], unmappable=42)
+        task_runs = bar.map([1, 2, 3], unmappable=42, deferred=True)

         assert len(task_runs) == 3

@@ -384,7 +384,9 @@ async def test_map_with_explicit_unmapped_kwargs(self):
         def bar(x: int, mappable: Iterable) -> Tuple[int, Iterable]:
             return (x, mappable)

-        task_runs = bar.map([1, 2, 3], mappable=unmapped(["some", "iterable"]))
+        task_runs = bar.map(
+            [1, 2, 3], mappable=unmapped(["some", "iterable"]), deferred=True
+        )

         assert len(task_runs) == 3

@@ -404,7 +406,9 @@ async def test_async_map_with_explicit_unmapped_kwargs(self):
         async def bar(x: int, mappable: Iterable) -> Tuple[int, Iterable]:
             return (x, mappable)

-        task_runs = bar.map([1, 2, 3], mappable=unmapped(["some", "iterable"]))
+        task_runs = bar.map(
+            [1, 2, 3], mappable=unmapped(["some", "iterable"]), deferred=True
+        )

         assert len(task_runs) == 3

diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 12eaea31ad28..08bfa4470514 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -18,7 +18,7 @@
 from prefect.client.orchestration import PrefectClient
 from prefect.client.schemas.filters import LogFilter, LogFilterFlowRunId
 from prefect.client.schemas.objects import StateType, TaskRunResult
-from prefect.context import TaskRunContext, get_run_context
+from prefect.context import FlowRunContext, TaskRunContext, get_run_context
 from prefect.exceptions import (
     MappingLengthMismatch,
     MappingMissingIterable,
@@ -76,6 +76,11 @@ def test_flow():
     return test_flow

+async def get_background_task_run_parameters(task, parameters_id):
+    factory = await ResultFactory.from_autonomous_task(task)
+    return await factory.read_parameters(parameters_id)
+
+
 class TestTaskName:
     def test_name_from_function(self):
         @task
@@ -203,19 +208,6 @@ async def bar():

         assert await bar() == 1

-    # Will not be supported in new engine
-    @pytest.mark.skip(reason="Fails with new engine, passed on old engine")
-    def test_async_task_called_inside_sync_flow(self):
-        @task
-        async def foo(x):
-            return x
-
-        @flow
-        def bar():
-            return foo(1)
-
-        assert bar() == 1
-
     def test_task_call_with_debug_mode(self):
         @task
         def foo(x):
@@ -371,21 +363,6 @@ async def bar():
         assert isinstance(task_state, State)
         assert await task_state.result() == 1

-    # Will not be supported in new engine
-    @pytest.mark.skip(reason="Fails with new engine, passed on old engine")
-    def test_async_task_run_inside_sync_flow(self):
-        @task
-        async def foo(x):
-            return x
-
-        @flow
-        def bar():
-            return foo(1, return_state=True)
-
-        task_state = bar()
-        assert isinstance(task_state, State)
-        assert task_state.result() == 1
-
     def test_task_failure_does_not_affect_flow(self):
         @task
         def foo():
@@ -425,6 +402,14 @@ def my_flow():

 class TestTaskSubmit:
+    def test_raises_outside_of_flow(self):
+        @task
+        def foo(x):
+            return x
+
+        with pytest.raises(RuntimeError):
+            foo.submit(1)
+
     async def test_sync_task_submitted_inside_sync_flow(self):
         @task
         def foo(x):
@@ -3165,6 +3150,67 @@ def my_flow():
         task_states = my_flow()
         assert [await state.result() for state in task_states] == [2, 3, 4]

+    def test_map_raises_outside_of_flow_when_not_deferred(self):
+        @task
+        def test_task(x):
+            print(x)
+
+        with pytest.raises(RuntimeError):
+            test_task.map([1, 2, 3])
+
+    async def test_deferred_map_outside_flow(self):
+        @task
+        def test_task(x):
+            print(x)
+
+        mock_task_run_id = uuid4()
+        mock_future = PrefectDistributedFuture(task_run_id=mock_task_run_id)
+        mapped_args = [1, 2, 3]
+
+        futures = test_task.map(x=mapped_args, wait_for=[mock_future], deferred=True)
+        assert all(isinstance(future, PrefectDistributedFuture) for future in futures)
+        for future, parameter_value in zip(futures, mapped_args):
+            assert await get_background_task_run_parameters(
+                test_task, future.state.state_details.task_parameters_id
+            ) == {
+                "parameters": {"x": parameter_value},
+                "wait_for": [mock_future],
+                "context": ANY,
+            }
+
+    async def test_deferred_map_inside_flow(self):
+        @task
+        def test_task(x):
+            print(x)
+
+        @flow
+        async def test_flow():
+            mock_task_run_id = uuid4()
+            mock_future = PrefectDistributedFuture(task_run_id=mock_task_run_id)
+            mapped_args = [1, 2, 3]
+
+            flow_run_context = FlowRunContext.get()
+            flow_run_id = flow_run_context.flow_run.id
+            futures = test_task.map(
+                x=mapped_args, wait_for=[mock_future], deferred=True
+            )
+            for future, parameter_value in zip(futures, mapped_args):
+                saved_data = await get_background_task_run_parameters(
+                    test_task, future.state.state_details.task_parameters_id
+                )
+                assert saved_data == {
+                    "parameters": {"x": parameter_value},
+                    "wait_for": [mock_future],
+                    "context": ANY,
+                }
+                # Context should contain the current flow run ID
+                assert (
+                    saved_data["context"]["flow_run_context"]["flow_run"]["id"]
+                    == flow_run_id
+                )
+
+        await test_flow()
+

 class TestTaskConstructorValidation:
     async def test_task_cannot_configure_too_many_custom_retry_delays(self):
@@ -4119,10 +4165,6 @@ def commit(txn):

 class TestApplyAsync:
-    async def get_background_task_run_parameters(self, task, parameters_id):
-        factory = await ResultFactory.from_autonomous_task(task)
-        return await factory.read_parameters(parameters_id)
-
     @pytest.mark.parametrize(
         "args, kwargs",
         [
@@ -4139,7 +4181,7 @@ def multiply(x, y):

         future = multiply.apply_async(args, kwargs)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             multiply, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 42}, "context": ANY}

@@ -4170,7 +4212,7 @@ def add(x, y=42):

         future = add.apply_async((42,))

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 42}, "context": ANY}

@@ -4181,7 +4223,7 @@ def add(x, y=42):

         future = add.apply_async((42,), {"y": 100})

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 100}, "context": ANY}

@@ -4192,7 +4234,7 @@ def add_em_up(*args):

         future = add_em_up.apply_async((42, 42))

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"args": (42, 42)}, "context": ANY}

@@ -4203,7 +4245,7 @@ def add_em_up(**kwargs):

         future = add_em_up.apply_async(kwargs={"x": 42, "y": 42})

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"kwargs": {"x": 42, "y": 42}}, "context": ANY}

@@ -4214,7 +4256,7 @@ def add_em_up(*args, **kwargs):

         future = add_em_up.apply_async((42,), {"y": 42})

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"args": (42,), "kwargs": {"y": 42}}, "context": ANY}

@@ -4228,7 +4270,7 @@ def multiply(x, y):

         future = multiply.apply_async((42, 42), wait_for=[wait_for_future])

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             multiply, future.state.state_details.task_parameters_id
         ) == {
             "parameters": {"x": 42, "y": 42},
@@ -4246,7 +4288,7 @@ def the_answer():

         future = the_answer.apply_async(wait_for=[wait_for_future])

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             the_answer, future.state.state_details.task_parameters_id
         ) == {"wait_for": [wait_for_future], "context": ANY}

@@ -4268,10 +4310,6 @@ def add(x, y):

 class TestDelay:
-    async def get_background_task_run_parameters(self, task, parameters_id):
-        factory = await ResultFactory.from_autonomous_task(task)
-        return await factory.read_parameters(parameters_id)
-
     @pytest.mark.parametrize(
         "args, kwargs",
         [
@@ -4288,7 +4326,7 @@ def multiply(x, y):

         future = multiply.delay(*args, **kwargs)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             multiply, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 42}, "context": ANY}

@@ -4319,7 +4357,7 @@ def add(x, y=42):

         future = add.delay(42)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 42}, "context": ANY}

@@ -4330,7 +4368,7 @@ def add(x, y=42):

         future = add.delay(42, y=100)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add, future.state.state_details.task_parameters_id
         ) == {"parameters": {"x": 42, "y": 100}, "context": ANY}

@@ -4341,7 +4379,7 @@ def add_em_up(*args):

         future = add_em_up.delay(42, 42)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"args": (42, 42)}, "context": ANY}

@@ -4352,7 +4390,7 @@ def add_em_up(**kwargs):

         future = add_em_up.delay(x=42, y=42)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"kwargs": {"x": 42, "y": 42}}, "context": ANY}

@@ -4363,6 +4401,6 @@ def add_em_up(*args, **kwargs):

         future = add_em_up.delay(42, y=42)

-        assert await self.get_background_task_run_parameters(
+        assert await get_background_task_run_parameters(
             add_em_up, future.state.state_details.task_parameters_id
         ) == {"parameters": {"args": (42,), "kwargs": {"y": 42}}, "context": ANY}

Add a `deferred` parameter to `Task.map` to control whether to defer or submit tasks
Currently, `Task.map` always submits tasks within a flow or defers tasks outside of a flow. But now that we support using `delay` and `apply_async` within flows correctly, a flow might want to defer tasks with `map` and may not be using `PrefectTaskRunner`, so we should allow controlling map behavior with a `deferred` parameter that defaults to `False`.


diff --git a/src/prefect/cli/deployment.py b/src/prefect/cli/deployment.py
index 398fd457653a..401c3dc80d70 100644
--- a/src/prefect/cli/deployment.py
+++ b/src/prefect/cli/deployment.py
@@ -22,7 +22,7 @@
 from prefect.cli._types import PrefectTyper
 from prefect.cli._utilities import exit_with_error, exit_with_success
 from prefect.cli.root import app
-from prefect.client.orchestration import ServerType, get_client
+from prefect.client.orchestration import get_client
 from prefect.client.schemas.filters import FlowFilter
 from prefect.client.schemas.objects import DeploymentSchedule
 from prefect.client.schemas.schedules import (
@@ -32,7 +32,7 @@
 )
 from prefect.client.utilities import inject_client
 from prefect.context import PrefectObjectRegistry, registry_from_script
-from prefect.deployments import Deployment, load_deployments_from_yaml
+from prefect.deployments import load_deployments_from_yaml
 from prefect.exceptions import (
     ObjectAlreadyExists,
     ObjectNotFound,
@@ -1103,135 +1103,6 @@ def _load_deployments(path: Path, quietly=False) -> PrefectObjectRegistry:
     return specs

-@deployment_app.command(
-    deprecated=True,
-    deprecated_start_date="Mar 2024",
-    deprecated_name="deployment apply",
-    deprecated_help="Use 'prefect deploy' to deploy flows via YAML instead.",
-)
-async def apply(
-    paths: List[str] = typer.Argument(
-        ...,
-        help="One or more paths to deployment YAML files.",
-    ),
-    upload: bool = typer.Option(
-        False,
-        "--upload",
-        help=(
-            "A flag that, when provided, uploads this deployment's files to remote"
-            " storage."
-        ),
-    ),
-    work_queue_concurrency: int = typer.Option(
-        None,
-        "--limit",
-        "-l",
-        help=(
-            "Sets the concurrency limit on the work queue that handles this"
-            " deployment's runs"
-        ),
-    ),
-):
-    """
-    Create or update a deployment from a YAML file.
-    """
-    deployment = None
-    async with get_client() as client:
-        for path in paths:
-            try:
-                deployment = await Deployment.load_from_yaml(path)
-                app.console.print(
-                    f"Successfully loaded {deployment.name!r}", style="green"
-                )
-            except Exception as exc:
-                exit_with_error(
-                    f"'{path!s}' did not conform to deployment spec: {exc!r}"
-                )
-
-            assert deployment
-
-            await create_work_queue_and_set_concurrency_limit(
-                deployment.work_queue_name,
-                deployment.work_pool_name,
-                work_queue_concurrency,
-            )
-
-            if upload:
-                if (
-                    deployment.storage
-                    and "put-directory" in deployment.storage.get_block_capabilities()
-                ):
-                    file_count = await deployment.upload_to_storage()
-                    if file_count:
-                        app.console.print(
-                            (
-                                f"Successfully uploaded {file_count} files to"
-                                f" {deployment.location}"
-                            ),
-                            style="green",
-                        )
-                else:
-                    app.console.print(
-                        (
-                            f"Deployment storage {deployment.storage} does not have"
-                            " upload capabilities; no files uploaded."
-                        ),
-                        style="red",
-                    )
-            await check_work_pool_exists(
-                work_pool_name=deployment.work_pool_name, client=client
-            )
-
-            if client.server_type != ServerType.CLOUD and deployment.triggers:
-                app.console.print(
-                    (
-                        "Deployment triggers are only supported on "
-                        f"Prefect Cloud. Triggers defined in {path!r} will be "
-                        "ignored."
-                    ),
-                    style="red",
-                )
-
-            deployment_id = await deployment.apply()
-            app.console.print(
-                (
-                    f"Deployment '{deployment.flow_name}/{deployment.name}'"
-                    f" successfully created with id '{deployment_id}'."
-                ),
-                style="green",
-            )
-
-            if PREFECT_UI_URL:
-                app.console.print(
-                    "View Deployment in UI:"
-                    f" {PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}"
-                )
-
-            if deployment.work_pool_name is not None:
-                await _print_deployment_work_pool_instructions(
-                    work_pool_name=deployment.work_pool_name, client=client
-                )
-            elif deployment.work_queue_name is not None:
-                app.console.print(
-                    "\nTo execute flow runs from this deployment, start an agent that"
-                    f" pulls work from the {deployment.work_queue_name!r} work queue:"
-                )
-                app.console.print(
-                    f"$ prefect agent start -q {deployment.work_queue_name!r}",
-                    style="blue",
-                )
-            else:
-                app.console.print(
-                    (
-                        "\nThis deployment does not specify a work queue name, which"
-                        " means agents will not be able to pick up its runs. To add a"
-                        " work queue, edit the deployment spec and re-run this command,"
-                        " or visit the deployment in the UI."
-                    ),
-                    style="red",
-                )
-
-
 @deployment_app.command()
 async def delete(
     name: Optional[str] = typer.Argument(

diff --git a/tests/cli/deployment/test_deployment_cli.py b/tests/cli/deployment/test_deployment_cli.py
index 57c19166a434..7b79206102b8 100644
--- a/tests/cli/deployment/test_deployment_cli.py
+++ b/tests/cli/deployment/test_deployment_cli.py
@@ -5,12 +5,10 @@

 from prefect import flow
 from prefect.client.orchestration import PrefectClient
-from prefect.deployments import Deployment
 from prefect.server.schemas.filters import DeploymentFilter, DeploymentFilterId
 from prefect.server.schemas.schedules import IntervalSchedule
 from prefect.settings import (
     PREFECT_API_SERVICES_TRIGGERS_ENABLED,
-    PREFECT_UI_URL,
     temporary_settings,
 )
 from prefect.testing.cli import invoke_and_assert
@@ -32,234 +30,6 @@ def fn():
     return fn

-def test_deployment_apply_prints_deprecation_warning(tmp_path, patch_import):
-    Deployment.build_from_flow(
-        flow=my_flow,
-        name="TEST",
-        flow_name="my_flow",
-        output=str(tmp_path / "test.yaml"),
-        work_queue_name="prod",
-    )
-
-    invoke_and_assert(
-        [
-            "deployment",
-            "apply",
-            str(tmp_path / "test.yaml"),
-        ],
-        temp_dir=tmp_path,
-        expected_output_contains=(
-            "WARNING: The 'deployment apply' command has been deprecated.",
-            "It will not be available after Sep 2024.",
-            "Use 'prefect deploy' to deploy flows via YAML instead.",
-        ),
-    )
-
-
-class TestOutputMessages:
-    def test_message_with_work_queue_name_from_python_build(
-        self, patch_import, tmp_path
-    ):
-        d = Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_queue_name="prod",
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_output_contains=[
-                (
-                    "To execute flow runs from this deployment, start an agent "
-                    f"that pulls work from the {d.work_queue_name!r} work queue:"
-                ),
-                f"$ prefect agent start -q {d.work_queue_name!r}",
-            ],
-        )
-
-    def test_message_with_prefect_agent_work_pool(
-        self, patch_import, tmp_path, prefect_agent_work_pool
-    ):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_pool_name=prefect_agent_work_pool.name,
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_output_contains=[
-                (
-                    "To execute flow runs from this deployment, start an agent that"
-                    f" pulls work from the {prefect_agent_work_pool.name!r} work pool:"
-                ),
-                f"$ prefect agent start -p {prefect_agent_work_pool.name!r}",
-            ],
-        )
-
-    def test_message_with_process_work_pool(
-        self, patch_import, tmp_path, process_work_pool
-    ):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_pool_name=process_work_pool.name,
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_output_contains=[
-                (
-                    "To execute flow runs from this deployment, start a worker "
-                    f"that pulls work from the {process_work_pool.name!r} work pool:"
-                ),
-                f"$ prefect worker start -p {process_work_pool.name!r}",
-            ],
-        )
-
-    def test_message_with_process_work_pool_without_workers_enabled(
-        self, patch_import, tmp_path, process_work_pool, disable_workers
-    ):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_pool_name=process_work_pool.name,
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_output_contains=[
-                (
-                    "\nTo execute flow runs from this deployment, please enable "
-                    "the workers CLI and start a worker that pulls work from the "
-                    f"{process_work_pool.name!r} work pool:"
-                ),
-                (
-                    "$ prefect config set PREFECT_EXPERIMENTAL_ENABLE_WORKERS=True\n"
-                    f"$ prefect worker start -p {process_work_pool.name!r}"
-                ),
-            ],
-        )
-
-    def test_linking_to_deployment_in_ui(
-        self,
-        patch_import,
-        tmp_path,
-        monkeypatch,
-    ):
-        with temporary_settings({PREFECT_UI_URL: "http://foo/bar"}):
-            Deployment.build_from_flow(
-                flow=my_flow,
-                name="TEST",
-                flow_name="my_flow",
-                output=str(tmp_path / "test.yaml"),
-                work_queue_name="prod",
-            )
-            invoke_and_assert(
-                [
-                    "deployment",
-                    "apply",
-                    str(tmp_path / "test.yaml"),
-                ],
-                expected_output_contains="http://foo/bar/deployments/deployment/",
-            )
-
-    def test_updating_work_queue_concurrency_from_python_build(
-        self, patch_import, tmp_path
-    ):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_queue_name="prod",
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-                "-l",
-                "42",
-            ],
-            expected_output_contains=[
-                "Updated concurrency limit on work queue 'prod' to 42",
-            ],
-        )
-
-    def test_message_with_missing_work_queue_name(self, patch_import, tmp_path):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_queue_name=None,
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_output_contains=(
-                (
-                    "This deployment does not specify a work queue name, which means"
-                    " agents will not be able to pick up its runs. To add a work queue,"
-                    " edit the deployment spec and re-run this command, or visit the"
-                    " deployment in the UI."
-                ),
-            ),
-        )
-
-    def test_message_with_missing_nonexistent_work_pool(self, patch_import, tmp_path):
-        Deployment.build_from_flow(
-            flow=my_flow,
-            name="TEST",
-            flow_name="my_flow",
-            output=str(tmp_path / "test.yaml"),
-            work_pool_name="gibberish",
-            schedule=IntervalSchedule(interval=60),
-        )
-        invoke_and_assert(
-            [
-                "deployment",
-                "apply",
-                str(tmp_path / "test.yaml"),
-            ],
-            expected_code=1,
-            expected_output_contains=(
-                [
-                    (
-                        "This deployment specifies a work pool name of 'gibberish', but"
-                        " no such work pool exists."
-                    ),
-                    "To create a work pool via the CLI:",
-                    "$ prefect work-pool create 'gibberish'",
-                ]
-            ),
-        )
-
-
 class TestDeploymentSchedules:
     @pytest.fixture(autouse=True)
     def enable_triggers(self):

Remove `prefect deployment apply` CLI from the `main` branch
Remove the `prefect.cli.deployment.apply` function and any references to it. Any private code that was used solely for that function should also be deleted. This CLI command will be included in 2.x releases, but will not be included in the 3.0 release.