Repository: PrefectHQ/prefect

Issue:
Support calling autonomous tasks
In addition to calling `Task.submit` from outside of a flow, you should be able to call the task directly, the same as you can from within a flow.


Discussion:

Resolving patch:
diff --git a/src/prefect/engine.py b/src/prefect/engine.py
--- a/src/prefect/engine.py
+++ b/src/prefect/engine.py
@@ -170,6 +170,7 @@
 from prefect.results import BaseResult, ResultFactory, UnknownResult
 from prefect.settings import (
     PREFECT_DEBUG_MODE,
+    PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
     PREFECT_LOGGING_LOG_PRINTS,
     PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD,
     PREFECT_TASKS_REFRESH_CACHE,
@@ -1369,10 +1370,27 @@ def enter_task_run_engine(
     flow_run_context = FlowRunContext.get()
 
     if not flow_run_context:
-        raise RuntimeError(
-            "Tasks cannot be run outside of a flow"
-            " - if you meant to submit an autonomous task, you need to set"
-            " `prefect config set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING=true`"
+        if (
+            not PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value()
+            or return_type == "future"
+            or mapped
+        ):
+            raise RuntimeError(
+                "Tasks cannot be run outside of a flow by default."
+                " If you meant to submit an autonomous task, you need to set"
+                " `prefect config set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING=true`"
+                " and use `your_task.submit()` instead of `your_task()`."
+                " Mapping autonomous tasks is not yet supported."
+            )
+        from prefect.task_engine import submit_autonomous_task_run_to_engine
+
+        return submit_autonomous_task_run_to_engine(
+            task=task,
+            parameters=parameters,
+            task_runner=task_runner,
+            wait_for=wait_for,
+            return_type=return_type,
+            client=get_client(),
         )
 
     if TaskRunContext.get():
@@ -1698,7 +1716,10 @@ async def create_task_run(
         task_inputs=task_inputs,
     )
 
-    logger.info(f"Created task run {task_run.name!r} for task {task.name!r}")
+    if flow_run_context.flow_run:
+        logger.info(f"Created task run {task_run.name!r} for task {task.name!r}")
+    else:
+        engine_logger.info(f"Created task run {task_run.name!r} for task {task.name!r}")
 
     return task_run
 
@@ -1716,7 +1737,7 @@ async def submit_task_run(
 
     if (
         task_runner.concurrency_type == TaskConcurrencyType.SEQUENTIAL
-        and not flow_run_context.autonomous_task_run
+        and flow_run_context.flow_run
     ):
         logger.info(f"Executing {task_run.name!r} immediately...")
 
diff --git a/src/prefect/task_engine.py b/src/prefect/task_engine.py
--- a/src/prefect/task_engine.py
+++ b/src/prefect/task_engine.py
@@ -1,5 +1,6 @@
 from contextlib import AsyncExitStack
 from typing import (
+    Any,
     Dict,
     Iterable,
     Optional,
@@ -9,6 +10,7 @@
 from typing_extensions import Literal
 
 from prefect._internal.concurrency.api import create_call, from_async, from_sync
+from prefect.client.orchestration import PrefectClient
 from prefect.client.schemas.objects import TaskRun
 from prefect.context import EngineContext
 from prefect.engine import (
@@ -30,11 +32,11 @@ async def submit_autonomous_task_run_to_engine(
     task: Task,
     task_run: TaskRun,
     task_runner: BaseTaskRunner,
-    parameters: Optional[Dict] = None,
+    parameters: Optional[Dict[str, Any]] = None,
     wait_for: Optional[Iterable[PrefectFuture]] = None,
     mapped: bool = False,
     return_type: EngineReturnType = "future",
-    client=None,
+    client: Optional[PrefectClient] = None,
 ) -> PrefectFuture:
     async with AsyncExitStack() as stack:
         parameters = parameters or {}
@@ -58,11 +60,17 @@ async def submit_autonomous_task_run_to_engine(
                 task_runner=task_runner,
             )
             if task.isasync:
-                future = await from_async.wait_for_call_in_loop_thread(begin_run)
+                future_result_or_state = await from_async.wait_for_call_in_loop_thread(
+                    begin_run
+                )
             else:
-                future = from_sync.wait_for_call_in_loop_thread(begin_run)
+                future_result_or_state = from_sync.wait_for_call_in_loop_thread(
+                    begin_run
+                )
 
-            await wait_for_task_runs_and_report_crashes(
-                task_run_futures=[future],
-                client=client,
-            )
+            if return_type == "future":
+                await wait_for_task_runs_and_report_crashes(
+                    task_run_futures=[future_result_or_state],
+                    client=client,
+                )
+            return future_result_or_state
diff --git a/src/prefect/tasks.py b/src/prefect/tasks.py
--- a/src/prefect/tasks.py
+++ b/src/prefect/tasks.py
@@ -555,6 +555,7 @@ def __call__(
         the result is wrapped in a Prefect State which provides error handling.
         """
         from prefect.engine import enter_task_run_engine
+        from prefect.task_engine import submit_autonomous_task_run_to_engine
         from prefect.task_runners import SequentialTaskRunner
 
         # Convert the call args/kwargs to a parameter dict
@@ -568,6 +569,21 @@ def __call__(
                 self.isasync, self.name, parameters, self.viz_return_value
             )
 
+        if (
+            PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value()
+            and not FlowRunContext.get()
+        ):
+            from prefect import get_client
+
+            return submit_autonomous_task_run_to_engine(
+                task=self,
+                task_run=None,
+                task_runner=SequentialTaskRunner(),
+                parameters=parameters,
+                return_type=return_type,
+                client=get_client(),
+            )
+
         return enter_task_run_engine(
             self,
             parameters=parameters,


Test patch:
diff --git a/tests/test_autonomous_tasks.py b/tests/test_autonomous_tasks.py
--- a/tests/test_autonomous_tasks.py
+++ b/tests/test_autonomous_tasks.py
@@ -183,13 +183,6 @@ async def test_async_task_submission_creates_a_scheduled_task_run(
     assert parameters == dict(x=42)
 
 
-async def test_task_submission_via_call_raises_error(
-    async_foo_task_with_result_storage,
-):
-    with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
-        async_foo_task_with_result_storage(42)
-
-
 async def test_task_submission_via_map_raises_error(async_foo_task_with_result_storage):
     with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
         async_foo_task_with_result_storage.map([42])
@@ -313,3 +306,17 @@ async def test_stuck_pending_tasks_are_reenqueued(
     # ...and it should be re-enqueued
     enqueued: TaskRun = await TaskQueue.for_key(task_run.task_key).get()
     assert enqueued.id == task_run.id
+
+
+class TestCall:
+    async def test_call(self, async_foo_task):
+        result = await async_foo_task(42)
+
+        assert result == 42
+
+    async def test_call_with_return_state(self, async_foo_task):
+        state = await async_foo_task(42, return_state=True)
+
+        assert state.is_completed()
+
+        assert await state.result() == 42