Repository: PrefectHQ/prefect

Issue:
Add `flow_run_url` to flow run notification template variables
### 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

2.x

### Describe the current behavior

Flow runs notifications currently allow the following fields to be provided as template variables

```
FLOW_RUN_NOTIFICATION_TEMPLATE_KWARGS = [
    "flow_run_notification_policy_id",
    "flow_id",
    "flow_name",
    "flow_run_url",
    "flow_run_id",
    "flow_run_name",
    "flow_run_parameters",
    "flow_run_state_type",
    "flow_run_state_name",
    "flow_run_state_timestamp",
    "flow_run_state_message",
]
```

### Describe the proposed behavior

Add `flow_run_url` to the allowed template variables.

The `FlowRunNotifications` service will be responsible for figuring out the correct flow run url and adding it to the templated variables.

### Example Use

If my flow run fails, I would like a slack notification with a link to the flow run ui page.

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/src/prefect/orion/models/flow_run_notification_policies.py b/src/prefect/orion/models/flow_run_notification_policies.py
--- a/src/prefect/orion/models/flow_run_notification_policies.py
+++ b/src/prefect/orion/models/flow_run_notification_policies.py
@@ -19,6 +19,8 @@
 
     Flow ID: {flow_id}
     Flow run ID: {flow_run_id}
+    Flow run URL: {flow_run_url}
+    State message: {flow_run_state_message}
     """
 )
 
diff --git a/src/prefect/orion/schemas/core.py b/src/prefect/orion/schemas/core.py
--- a/src/prefect/orion/schemas/core.py
+++ b/src/prefect/orion/schemas/core.py
@@ -21,6 +21,7 @@
     "flow_run_notification_policy_id",
     "flow_id",
     "flow_name",
+    "flow_run_url",
     "flow_run_id",
     "flow_run_name",
     "flow_run_parameters",
diff --git a/src/prefect/orion/services/flow_run_notifications.py b/src/prefect/orion/services/flow_run_notifications.py
--- a/src/prefect/orion/services/flow_run_notifications.py
+++ b/src/prefect/orion/services/flow_run_notifications.py
@@ -2,6 +2,7 @@
 A service that checks for flow run notifications and sends them.
 """
 import asyncio
+from uuid import UUID
 
 import sqlalchemy as sa
 
@@ -9,6 +10,7 @@
 from prefect.orion.database.dependencies import inject_db
 from prefect.orion.database.interface import OrionDBInterface
 from prefect.orion.services.loop_service import LoopService
+from prefect.settings import PREFECT_API_URL
 
 
 class FlowRunNotifications(LoopService):
@@ -89,16 +91,7 @@ async def send_flow_run_notification(
                 )
             )
 
-            message_template = (
-                notification.flow_run_notification_policy_message_template
-                or models.flow_run_notification_policies.DEFAULT_MESSAGE_TEMPLATE
-            )
-            message = message_template.format(
-                **{
-                    k: notification[k]
-                    for k in schemas.core.FLOW_RUN_NOTIFICATION_TEMPLATE_KWARGS
-                }
-            )
+            message = self.construct_notification_message(notification=notification)
             await block.notify(
                 subject="Prefect flow run notification",
                 body=message,
@@ -116,6 +109,42 @@ async def send_flow_run_notification(
                 exc_info=True,
             )
 
+    def construct_notification_message(self, notification) -> str:
+        """
+        Construct the message for a flow run notification, including
+        templating any variables.
+        """
+        message_template = (
+            notification.flow_run_notification_policy_message_template
+            or models.flow_run_notification_policies.DEFAULT_MESSAGE_TEMPLATE
+        )
+
+        # create a dict from the sqlalchemy object for templating
+        notification_dict = dict(notification)
+        # add the flow run url to the info
+        notification_dict["flow_run_url"] = self.get_ui_url_for_flow_run_id(
+            flow_run_id=notification_dict["flow_run_id"]
+        )
+
+        message = message_template.format(
+            **{
+                k: notification_dict[k]
+                for k in schemas.core.FLOW_RUN_NOTIFICATION_TEMPLATE_KWARGS
+            }
+        )
+        return message
+
+    def get_ui_url_for_flow_run_id(self, flow_run_id: UUID) -> str:
+        """
+        Returns a link to the flow run view of the given flow run id.
+
+        Args:
+            flow_run_id: the flow run id.
+        """
+        api_url = PREFECT_API_URL.value() or "http://ephemeral-orion/api"
+        ui_url = api_url.strip("/api")
+        return f"{ui_url}/flow-run/{flow_run_id}"
+
 
 if __name__ == "__main__":
     asyncio.run(FlowRunNotifications().start())


Test patch:
diff --git a/tests/orion/services/test_flow_run_notifications.py b/tests/orion/services/test_flow_run_notifications.py
--- a/tests/orion/services/test_flow_run_notifications.py
+++ b/tests/orion/services/test_flow_run_notifications.py
@@ -107,7 +107,7 @@ async def test_service_uses_message_template(
         session=session,
         flow_run_notification_policy_id=completed_policy.id,
         flow_run_notification_policy=schemas.actions.FlowRunNotificationPolicyUpdate(
-            message_template="Hi there {flow_run_name}"
+            message_template="Hi there {flow_run_name}! Also the url works: {flow_run_url}"
         ),
     )
 
@@ -118,9 +118,13 @@ async def test_service_uses_message_template(
     await session.commit()
 
     await FlowRunNotifications(handle_signals=False).start(loops=1)
+    expected_url = FlowRunNotifications(
+        handle_signals=False
+    ).get_ui_url_for_flow_run_id(flow_run_id=flow_run.id)
 
     captured = capsys.readouterr()
     assert f"Hi there {flow_run.name}" in captured.out
+    assert f"Also the url works: {expected_url}" in captured.out
 
 
 async def test_service_sends_multiple_notifications(