Repository: PrefectHQ/prefect

Issue:
cannot call flows in standalone tasks
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

since #12548 and #12559, we've removed the restraint that you cannot call flows from tasks. This doesn't inherently break when you have a wrapping flow, but does when its from a standalone task

in multiple places in the engine, we're trying to access attrs on a `parent_flow_run_context` that does not exist

we need some `getattr` for conditional handling of these cases

### Reproduction

```python3
In [1]: from prefect import flow, task

In [2]: @task
   ...: def calls_flow():
   ...:     @flow
   ...:     def foo():
   ...:         pass
   ...:     foo()
   ...:

In [3]: calls_flow()
```


### Error

```python3
File ~/github.com/prefecthq/prefect/src/prefect/engine.py:619, in create_and_begin_subflow_run(flow, parameters, wait_for, return_type, client, user_thread)
    616 if wait_for:
    617     task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
--> 619 rerunning = parent_flow_run_context.flow_run.run_count > 1
    621 # Generate a task in the parent flow run to represent the result of the subflow run
    622 dummy_task = Task(name=flow.name, fn=flow.fn, version=flow.version)

AttributeError: 'NoneType' object has no attribute 'run_count'
```


### Versions

```Text
» prefect version
Version:             2.16.9
API version:         0.8.4
Python version:      3.12.2
Git commit:          db20fe46
Built:               Fri, Apr 5, 2024 11:15 AM
OS/Arch:             darwin/arm64
Profile:             pong
Server type:         cloud
```


### Additional context

_No response_


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
@@ -163,7 +163,6 @@
 from prefect.results import ResultFactory, UnknownResult
 from prefect.settings import (
     PREFECT_DEBUG_MODE,
-    PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
     PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD,
     PREFECT_TASKS_REFRESH_CACHE,
     PREFECT_UI_URL,
@@ -617,13 +616,21 @@ async def create_and_begin_subflow_run(
     if wait_for:
         task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
 
-    rerunning = parent_flow_run_context.flow_run.run_count > 1
+    rerunning = (
+        parent_flow_run_context.flow_run.run_count > 1
+        if getattr(parent_flow_run_context, "flow_run", None)
+        else False
+    )
 
     # Generate a task in the parent flow run to represent the result of the subflow run
     dummy_task = Task(name=flow.name, fn=flow.fn, version=flow.version)
     parent_task_run = await client.create_task_run(
         task=dummy_task,
-        flow_run_id=parent_flow_run_context.flow_run.id,
+        flow_run_id=(
+            parent_flow_run_context.flow_run.id
+            if getattr(parent_flow_run_context, "flow_run", None)
+            else None
+        ),
         dynamic_key=_dynamic_key_for_task_run(parent_flow_run_context, dummy_task),
         task_inputs=task_inputs,
         state=Pending(),
@@ -851,7 +858,7 @@ async def orchestrate_flow_run(
                 if parent_call and (
                     not parent_flow_run_context
                     or (
-                        parent_flow_run_context
+                        getattr(parent_flow_run_context, "flow", None)
                         and parent_flow_run_context.flow.isasync == flow.isasync
                     )
                 ):
@@ -1372,17 +1379,11 @@ def enter_task_run_engine(
     flow_run_context = FlowRunContext.get()
 
     if not flow_run_context:
-        if (
-            not PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value()
-            or return_type == "future"
-            or mapped
-        ):
+        if 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"
+                " If you meant to submit a background 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
 


Test patch:
diff --git a/tests/events/client/instrumentation/test_task_run_state_change_events.py b/tests/events/client/instrumentation/test_task_run_state_change_events.py
--- a/tests/events/client/instrumentation/test_task_run_state_change_events.py
+++ b/tests/events/client/instrumentation/test_task_run_state_change_events.py
@@ -3,6 +3,7 @@
 from prefect import flow, task
 from prefect.events.clients import AssertingEventsClient
 from prefect.events.worker import EventsWorker
+from prefect.filesystems import LocalFileSystem
 from prefect.settings import (
     PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
     temporary_settings,
@@ -150,7 +151,10 @@ async def test_background_task_state_changes(
     prefect_client,
     enable_task_scheduling,
 ):
-    @task
+    storage = LocalFileSystem(basepath="/tmp/prefect")
+    storage.save("test")
+
+    @task(result_storage=storage)
     def foo():
         pass
 
@@ -164,14 +168,11 @@ def foo():
 
     events = sorted(asserting_events_worker._client.events, key=lambda e: e.occurred)
 
-    assert len(events) == 5  # 4 state changes + 1 block save
-
-    assert len(task_run_states) == 4
+    assert len(task_run_states) == len(events) == 4
 
     assert [e.event for e in events] == [
         "prefect.task-run.Scheduled",
         "prefect.task-run.Pending",
-        "prefect.block.local-file-system.save.called",
         "prefect.task-run.Running",
         "prefect.task-run.Completed",
     ]
diff --git a/tests/test_background_tasks.py b/tests/test_background_tasks.py
--- a/tests/test_background_tasks.py
+++ b/tests/test_background_tasks.py
@@ -95,12 +95,6 @@ def async_foo_task_with_result_storage(async_foo_task, local_filesystem):
     return async_foo_task.with_options(result_storage=local_filesystem)
 
 
-def test_task_submission_fails_when_experimental_flag_off(foo_task):
-    with temporary_settings({PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING: False}):
-        with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
-            foo_task.submit(42)
-
-
 def test_task_submission_with_parameters_uses_default_storage(foo_task):
     foo_task_without_result_storage = foo_task.with_options(result_storage=None)
     task_run = foo_task_without_result_storage.submit(42)
diff --git a/tests/test_task_server.py b/tests/test_task_server.py
--- a/tests/test_task_server.py
+++ b/tests/test_task_server.py
@@ -4,7 +4,7 @@
 import pytest
 
 import prefect.results
-from prefect import task
+from prefect import flow, task
 from prefect._internal.pydantic import HAS_PYDANTIC_V2
 from prefect.client.schemas.objects import TaskRun
 from prefect.exceptions import MissingResult
@@ -528,3 +528,24 @@ def outer_task(x):
         assert updated_task_run.state.is_completed()
 
         assert await updated_task_run.state.result() == 42
+
+    async def test_nested_flow_run_via_task_server(self, prefect_client):
+        @flow
+        def inner_flow(x):
+            return x
+
+        @task
+        def background_task(x):
+            return inner_flow(x)
+
+        task_server = TaskServer(background_task)
+
+        task_run = background_task.submit(42)
+
+        await task_server.execute_task_run(task_run)
+
+        updated_task_run = await prefect_client.read_task_run(task_run.id)
+
+        assert updated_task_run.state.is_completed()
+
+        assert await updated_task_run.state.result() == 42
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -163,14 +163,6 @@ def my_task():
 
 
 class TestTaskCall:
-    def test_task_called_outside_flow_raises(self):
-        @task
-        def foo():
-            pass
-
-        with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
-            foo()
-
     def test_sync_task_called_inside_sync_flow(self):
         @task
         def foo(x):
@@ -331,14 +323,6 @@ def bar():
 
 
 class TestTaskRun:
-    def test_task_run_outside_flow_raises(self):
-        @task
-        def foo():
-            pass
-
-        with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
-            foo()
-
     def test_sync_task_run_inside_sync_flow(self):
         @task
         def foo(x):
@@ -430,14 +414,6 @@ def my_flow():
 
 
 class TestTaskSubmit:
-    def test_task_submitted_outside_flow_raises(self):
-        @task
-        def foo():
-            pass
-
-        with pytest.raises(RuntimeError, match="Tasks cannot be run outside of a flow"):
-            foo()
-
     def test_sync_task_submitted_inside_sync_flow(self):
         @task
         def foo(x):