diff --git a/src/Frontend/package-lock.json b/src/Frontend/package-lock.json
index 7e66459fd..095ff0231 100644
--- a/src/Frontend/package-lock.json
+++ b/src/Frontend/package-lock.json
@@ -11,6 +11,7 @@
         "@codemirror/lang-json": "^6.0.1",
         "@codemirror/lang-xml": "^6.1.0",
         "@codemirror/legacy-modes": "^6.5.0",
+        "@dagrejs/dagre": "^1.1.4",
         "@tinyhttp/content-disposition": "^2.2.2",
         "@vue-flow/controls": "^1.1.2",
         "@vue-flow/core": "^1.42.5",
@@ -459,6 +460,24 @@
         "node": ">=18"
       }
     },
+    "node_modules/@dagrejs/dagre": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz",
+      "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==",
+      "license": "MIT",
+      "dependencies": {
+        "@dagrejs/graphlib": "2.2.4"
+      }
+    },
+    "node_modules/@dagrejs/graphlib": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
+      "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">17.0.0"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.25.1",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
diff --git a/src/Frontend/package.json b/src/Frontend/package.json
index c558c45ed..f799210a3 100644
--- a/src/Frontend/package.json
+++ b/src/Frontend/package.json
@@ -20,6 +20,7 @@
     "@codemirror/lang-json": "^6.0.1",
     "@codemirror/lang-xml": "^6.1.0",
     "@codemirror/legacy-modes": "^6.5.0",
+    "@dagrejs/dagre": "^1.1.4",
     "@tinyhttp/content-disposition": "^2.2.2",
     "@vue-flow/controls": "^1.1.2",
     "@vue-flow/core": "^1.42.5",
diff --git a/src/Frontend/src/assets/endpoint-lost.svg b/src/Frontend/src/assets/endpoint-lost.svg
index 2be2dd02c..a6c73ab95 100644
--- a/src/Frontend/src/assets/endpoint-lost.svg
+++ b/src/Frontend/src/assets/endpoint-lost.svg
@@ -1 +1,10 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 22 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="endpoint-lost"><path d="M19.054,11.155l1.937,-1.938l0.861,0.861l-1.937,1.937l1.937,1.938l-0.861,0.861l-1.937,-1.938l-1.937,1.938l-0.861,-0.861l1.937,-1.938l-1.937,-1.937l0.861,-0.861l1.937,1.938Z" style="fill:#ce4844;"/><g><path d="M11.395,10.953l0,-2.978l-3.971,-0l0,-4.964l3.971,0l0,-2.978l-10.92,0l0,10.92l10.92,0Z" style="fill:#ce4844;fill-rule:nonzero;"/><rect x="8.417" y="4.004" width="5.956" height="2.978" style="fill:#ce4844;"/></g></g></svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 22 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g id="endpoint-lost">
+        <path d="M19.054,11.155l1.937,-1.938l0.861,0.861l-1.937,1.937l1.937,1.938l-0.861,0.861l-1.937,-1.938l-1.937,1.938l-0.861,-0.861l1.937,-1.938l-1.937,-1.937l0.861,-0.861l1.937,1.938Z" style="fill:#ce4844;"/>
+        <g>
+            <path d="M11.395,10.953l0,-2.978l-3.971,-0l0,-4.964l3.971,0l0,-2.978l-10.92,0l0,10.92l10.92,0Z" style="fill:#ce4844;fill-rule:nonzero;"/>
+            <rect x="8.417" y="4.004" width="5.956" height="2.978" style="fill:#ce4844;"/>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/src/Frontend/src/assets/saga-completed.svg b/src/Frontend/src/assets/saga-completed.svg
new file mode 100644
index 000000000..e63f5a58e
--- /dev/null
+++ b/src/Frontend/src/assets/saga-completed.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <rect width="15" height="15" style="fill:#333333;fill-rule:nonzero;"/>
+    <rect x="4" y="4" width="7" height="7" style="fill:#E6E6E6;fill-rule:nonzero;"/>
+</svg>
diff --git a/src/Frontend/src/assets/saga-initiated.svg b/src/Frontend/src/assets/saga-initiated.svg
new file mode 100644
index 000000000..969373a9c
--- /dev/null
+++ b/src/Frontend/src/assets/saga-initiated.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+   <rect width="15" height="15" style="fill:#333333;fill-rule:nonzero;"/>
+    <polygon points="4.986,4 11.048,7.5 4.986,11" style="fill:#E6E6E6;fill-rule:nonzero;"/>
+</svg>
diff --git a/src/Frontend/src/assets/saga-trigger.svg b/src/Frontend/src/assets/saga-trigger.svg
new file mode 100644
index 000000000..0b809af35
--- /dev/null
+++ b/src/Frontend/src/assets/saga-trigger.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <rect width="15" height="15" style="fill:#333333"/>
+    <line x1="2.986" x2="9.051" y1="12" y2="5.935" stroke-width="2" stroke-miterlimit="10" style="stroke:#E6E6E6;fill-rule:nonzero;"/>
+    <polygon points="10.493,8.571 11.986,3 6.415,4.492" style="fill:#E6E6E6;fill-rule:nonzero;"/>
+</svg>
diff --git a/src/Frontend/src/assets/saga.svg b/src/Frontend/src/assets/saga.svg
deleted file mode 100644
index f284fcc52..000000000
--- a/src/Frontend/src/assets/saga.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
-    <g>
-        <path d="M6,5.362C4.217,4.655 3,3.411 3,0L1,0C1,5.082 3.461,6.665 6,7.482L6,5.362Z" style="fill-rule:nonzero;"/>
-        <path d="M14.911,12.17C14.382,8.505 12.209,7.217 10,6.508L10,8.617C11.462,9.184 12.54,10.097 12.906,12.236L11.724,12.275L14.042,16L16.11,12.13L14.911,12.17Z" style="fill-rule:nonzero;"/>
-    </g>
-    <path d="M9,12.201L9,0L7,0L7,12.201L5.806,12.201L8,16L10.194,12.201L9,12.201Z" style="fill-rule:nonzero;"/>
-</svg>
diff --git a/src/Frontend/src/components/messages/FlowDiagram.vue b/src/Frontend/src/components/messages/FlowDiagram.vue
index 210accdb2..e2eaea508 100644
--- a/src/Frontend/src/components/messages/FlowDiagram.vue
+++ b/src/Frontend/src/components/messages/FlowDiagram.vue
@@ -371,7 +371,7 @@ function typeIcon(type: MessageType) {
 }
 
 .pa-flow-saga {
-  background-image: url("@/assets/saga.svg");
+  background-image: url("@/assets/saga-completed.svg");
   background-position: center;
   background-repeat: no-repeat;
   height: 15px;
diff --git a/src/Frontend/src/components/messages2/FlowDiagram.vue b/src/Frontend/src/components/messages2/FlowDiagram.vue
deleted file mode 100644
index 44a004bb5..000000000
--- a/src/Frontend/src/components/messages2/FlowDiagram.vue
+++ /dev/null
@@ -1,457 +0,0 @@
-<script setup lang="ts">
-import { onMounted, ref } from "vue";
-import { type DefaultEdge, MarkerType, type Node, type Styles, VueFlow } from "@vue-flow/core";
-import TimeSince from "../TimeSince.vue";
-import routeLinks from "@/router/routeLinks";
-import Message, { MessageStatus } from "@/resources/Message";
-import { NServiceBusHeaders } from "@/resources/Header";
-import { ControlButton, Controls } from "@vue-flow/controls";
-import { useMessageStore } from "@/stores/MessageStore";
-import LoadingSpinner from "@/components/LoadingSpinner.vue";
-import { storeToRefs } from "pinia";
-import EndpointDetails from "@/resources/EndpointDetails.ts";
-import { hexToCSSFilter } from "hex-to-css-filter";
-import TextEllipses from "@/components/TextEllipses.vue";
-
-enum MessageType {
-  Event = "Event message",
-  Timeout = "Timeout message",
-  Command = "Command message",
-}
-
-interface MappedMessage {
-  nodeName: string;
-  id: string;
-  messageId: string;
-  sendingEndpoint: EndpointDetails;
-  receivingEndpoint: EndpointDetails;
-  parentId: string;
-  parentEndpoint: string;
-  type: MessageType;
-  isError: boolean;
-  sagaName: string;
-  link: {
-    name: string;
-    nodeName: string;
-  };
-  timeSent: string;
-  level: number;
-  width: number;
-  XPos: number;
-}
-
-const nodeSpacingX = 300;
-const nodeSpacingY = 200;
-
-const store = useMessageStore();
-const { state } = storeToRefs(store);
-
-async function getConversation(conversationId: string) {
-  await store.loadConversation(conversationId);
-
-  return store.conversationData.data;
-}
-
-function mapMessage(message: Message): MappedMessage {
-  let parentId = "",
-    parentEndpoint = "",
-    sagaName = "";
-  const header = message.headers.find((header) => header.key === NServiceBusHeaders.RelatedTo);
-  if (header) {
-    parentId = header.value ?? "";
-    parentEndpoint = message.headers.find((h) => h.key === "NServiceBus.OriginatingEndpoint")?.value ?? "";
-  }
-
-  const sagaHeader = message.headers.find((header) => header.key === NServiceBusHeaders.OriginatingSagaType);
-  if (sagaHeader) {
-    sagaName = sagaHeader.value?.split(", ")[0] ?? "";
-  }
-
-  const type = (() => {
-    if (message.headers.find((header) => header.key === NServiceBusHeaders.MessageIntent)?.value === "Publish") return MessageType.Event;
-    else if (message.headers.find((header) => header.key === NServiceBusHeaders.IsSagaTimeoutMessage)?.value?.toLowerCase() === "true") return MessageType.Timeout;
-    return MessageType.Command;
-  })();
-
-  return {
-    nodeName: message.message_type,
-    id: message.id,
-    messageId: message.message_id,
-    sendingEndpoint: message.sending_endpoint,
-    receivingEndpoint: message.receiving_endpoint,
-    parentId,
-    parentEndpoint,
-    type,
-    isError: message.status !== MessageStatus.Successful && message.status !== MessageStatus.ResolvedSuccessfully,
-    sagaName,
-    level: 0,
-    width: 0,
-    XPos: 0,
-    link: {
-      name: `Link ${message.id}`,
-      nodeName: message.id,
-    },
-    timeSent: message.time_sent,
-  };
-}
-
-function constructNodes(mappedMessages: MappedMessage[]): Node<MappedMessage>[] {
-  return (
-    mappedMessages
-      //group by level
-      .reduce((groups: MappedMessage[][], message: MappedMessage) => {
-        groups[message.level] = [...(groups[message.level] ?? []), message];
-        return groups;
-      }, [])
-      //ensure each level has their items in the same "grouped" order as the level above
-      .map((group, level, messagesByLevel) => {
-        const previousLevel = level > 0 ? messagesByLevel[level - 1] : null;
-        return group.sort(
-          (a, b) =>
-            (previousLevel?.findIndex((plMessage) => a.parentId === plMessage.messageId && a.parentEndpoint === plMessage.receivingEndpoint.name) ?? 1) -
-            (previousLevel?.findIndex((plMessage) => b.parentId === plMessage.messageId && b.parentEndpoint === plMessage.receivingEndpoint.name) ?? 1)
-        );
-      })
-      //flatten to actual flow diagram nodes, with positioning based on parent node/level
-      .flatMap((group, level, messagesByLevel) => {
-        const previousLevel = level > 0 ? messagesByLevel[level - 1] : null;
-        return group.reduce(
-          ({ result, currentWidth, previousParent }, message) => {
-            //position on current level needs to be based on parent Node, so see if one exists
-            const parentMessage = previousLevel?.find((plMessage) => message.parentId === plMessage.messageId && message.parentEndpoint === plMessage.receivingEndpoint.name) ?? null;
-            //if the current parent node is the same as the previous parent node, then the current position needs to be to the right of siblings
-            const currentParentWidth = previousParent === parentMessage ? currentWidth : 0;
-            const startX = parentMessage == null ? 0 : parentMessage.XPos - parentMessage.width / 2;
-            //store the position of the node against the message, so child nodes can use it to determine their start position
-            message.XPos = startX + (currentParentWidth + message.width / 2);
-            return {
-              result: [
-                ...result,
-                {
-                  id: `${message.messageId}##${message.receivingEndpoint.name}`,
-                  type: "message",
-                  data: message,
-                  label: message.nodeName,
-                  position: { x: message.XPos * nodeSpacingX, y: message.level * nodeSpacingY },
-                },
-              ],
-              currentWidth: currentParentWidth + message.width,
-              previousParent: parentMessage,
-            };
-          },
-          { result: [] as Node[], currentWidth: 0, previousParent: null as MappedMessage | null }
-        ).result;
-      })
-  );
-}
-
-function constructEdges(mappedMessages: MappedMessage[]): DefaultEdge[] {
-  return mappedMessages
-    .filter((message) => message.parentId)
-    .map((message) => ({
-      id: `${message.parentId}##${message.messageId}`,
-      source: `${message.parentId}##${message.parentEndpoint}`,
-      target: `${message.messageId}##${message.receivingEndpoint.name}`,
-      markerEnd: MarkerType.ArrowClosed,
-      style: {
-        "stroke-dasharray": message.type === MessageType.Event && "5, 3",
-      } as Styles,
-    }));
-}
-
-const elements = ref<(Node | DefaultEdge)[]>([]);
-
-onMounted(async () => {
-  if (!state.value.data.conversation_id) return;
-
-  const messages = await getConversation(state.value.data.conversation_id);
-  const mappedMessages = messages.map(mapMessage);
-
-  const assignDescendantLevelsAndWidth = (message: MappedMessage, level = 0) => {
-    message.level = level;
-    const children = mappedMessages.filter((mm) => mm.parentId === message.messageId && mm.parentEndpoint === message.receivingEndpoint.name);
-    message.width =
-      children.length === 0
-        ? 1 //leaf node
-        : children.map((child) => (child.width === 0 ? assignDescendantLevelsAndWidth(child, level + 1) : child)).reduce((sum, { width }) => sum + width, 0);
-    return message;
-  };
-  for (const root of mappedMessages.filter((message) => !message.parentId)) {
-    assignDescendantLevelsAndWidth(root);
-  }
-
-  const nodes = constructNodes(mappedMessages);
-  const edges = constructEdges(nodes.map((n) => n.data as MappedMessage));
-
-  elements.value = [...nodes, ...edges];
-});
-
-function typeIcon(type: MessageType) {
-  switch (type) {
-    case MessageType.Timeout:
-      return "pa-flow-timeout";
-    case MessageType.Event:
-      return "pa-flow-event";
-    default:
-      return "pa-flow-command";
-  }
-}
-
-const showAddress = ref(false);
-
-function toggleAddress() {
-  showAddress.value = !showAddress.value;
-}
-
-const blackColor = hexToCSSFilter("#000000").filter;
-const greenColor = hexToCSSFilter("#00c468").filter;
-</script>
-
-<template>
-  <div v-if="store.conversationData.failed_to_load" class="alert alert-info">FlowDiagram data is unavailable.</div>
-  <LoadingSpinner v-else-if="store.conversationData.loading" />
-  <div v-else id="tree-container">
-    <VueFlow v-model="elements" :min-zoom="0.1" :fit-view-on-init="true" :only-render-visible-elements="true">
-      <Controls position="top-left" class="controls">
-        <ControlButton v-tippy="showAddress ? `Hide endpoints` : `Show endpoints`" @click="toggleAddress">
-          <i class="fa pa-flow-endpoint" :style="{ filter: showAddress ? greenColor : blackColor }"></i>
-        </ControlButton>
-      </Controls>
-      <template #node-message="{ data }: { data: MappedMessage }">
-        <div v-if="showAddress">
-          <TextEllipses class="address" :text="`${data.sendingEndpoint.name}@${data.sendingEndpoint.host}`" />
-        </div>
-        <div class="node" :class="{ error: data.isError, 'current-message': data.id === store.state.data.id }">
-          <div class="node-text">
-            <i v-if="data.isError" class="fa pa-flow-failed" />
-            <i class="fa" :class="typeIcon(data.type)" v-tippy="data.type" />
-            <div class="lead">
-              <strong>
-                <RouterLink v-if="data.isError" :to="{ path: routeLinks.messages.failedMessage.link(data.id) }"><TextEllipses style="width: 204px" :text="data.nodeName" ellipses-style="LeftSide" /></RouterLink>
-                <RouterLink v-else :to="{ path: routeLinks.messages.successMessage.link(data.messageId, data.id) }"><TextEllipses style="width: 204px" :text="data.nodeName" ellipses-style="LeftSide" /></RouterLink>
-              </strong>
-            </div>
-            <div class="time-sent">
-              <time-since class="time-since" :date-utc="data.timeSent" />
-            </div>
-            <template v-if="data.sagaName">
-              <i class="fa pa-flow-saga" />
-              <div class="saga lead"><TextEllipses style="width: 182px" :text="data.sagaName" ellipses-style="LeftSide" /></div>
-            </template>
-          </div>
-        </div>
-        <div v-if="showAddress">
-          <TextEllipses class="address" :text="`${data.receivingEndpoint.name}@${data.receivingEndpoint.host}`" />
-        </div>
-      </template>
-    </VueFlow>
-  </div>
-</template>
-
-<style>
-@import "@vue-flow/core/dist/style.css";
-@import "@vue-flow/core/dist/theme-default.css";
-@import "@vue-flow/controls/dist/style.css";
-</style>
-
-<style scoped>
-@import "../list.css";
-
-.controls {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: center;
-}
-
-#tree-container {
-  width: 90vw;
-  height: 60vh;
-}
-
-.node {
-  --vf-handle: var(--vf-node-color, #1a192b);
-  --vf-box-shadow: var(--vf-node-color, #1a192b);
-  background: var(--vf-node-bg);
-  border-color: var(--vf-node-color, #1a192b);
-  border-radius: 3px;
-  font-size: 12px;
-  border-width: 1px;
-  border-style: solid;
-  color: var(--vf-node-text);
-  text-align: left;
-}
-
-.node {
-  background-color: #fff;
-  border-color: #cccbcc;
-  border-width: 3px;
-}
-
-.node .error {
-  border-color: red;
-}
-
-.node text {
-  font: 12px sans-serif;
-}
-
-.node .time-sent .time-since {
-  margin-left: 20px;
-  padding-top: 0;
-  color: #777f7f;
-  text-transform: capitalize;
-}
-
-.node-text {
-  padding: 3px 8px 1px;
-}
-
-.node-text i {
-  display: inline-block;
-  position: relative;
-  top: -1px;
-  margin-right: 5px;
-  filter: brightness(0) saturate(100%) invert(0%) sepia(0%) saturate(0%) hue-rotate(346deg) brightness(104%) contrast(104%);
-}
-
-.node-text .lead {
-  display: inline-block;
-  position: relative;
-  top: 4px;
-}
-
-.error .node-text .lead,
-.current-message.error .node-text .lead {
-  width: 184px;
-}
-
-.node-text .lead.saga {
-  font-weight: normal;
-}
-
-.address {
-  color: #777f7f;
-  font-size: 0.8em;
-  width: 264px;
-}
-
-.current-message {
-  border-color: #cccbcc;
-  background-color: #cccbcc !important;
-}
-
-.current-message.error {
-  border-color: #be514a;
-  background-color: #be514a !important;
-}
-
-.current-message.error .node-text,
-.current-message .node-text .lead {
-  color: #fff !important;
-}
-
-.error .node-text i:not(.pa-flow-saga) {
-  filter: brightness(0) saturate(100%) invert(46%) sepia(9%) saturate(4493%) hue-rotate(317deg) brightness(81%) contrast(82%);
-}
-
-.current-message.error .node-text i {
-  color: #fff;
-  filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7475%) hue-rotate(21deg) brightness(100%) contrast(106%);
-}
-
-.current-message.error .node-text strong {
-  color: #fff;
-}
-
-.current-message.error .node-text .time-sent .time-since {
-  color: #ffcecb !important;
-}
-
-.error {
-  border-color: #be514a;
-}
-
-.current-message.error .node-text a {
-  color: #fff;
-}
-
-.current-message.error .node-text a:hover {
-  cursor: text;
-  text-decoration: none;
-}
-
-.node-text a {
-  color: #000;
-}
-
-.error .node-text a {
-  color: #be514a;
-}
-
-.error .node-text .time-sent .time-since {
-  color: #be514a;
-}
-
-.error .node-text .lead.saga {
-  color: #be514a;
-}
-
-.error .node-text a:hover {
-  text-decoration: underline;
-}
-
-.pa-flow-endpoint {
-  background-image: url("@/assets/endpoint.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-}
-
-.pa-flow-failed {
-  background-image: url("@/assets/failed-msg.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-}
-
-.pa-flow-saga {
-  background-image: url("@/assets/saga.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-  margin-left: 20px;
-}
-
-.pa-flow-timeout {
-  background-image: url("@/assets/timeout.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-}
-
-.pa-flow-event {
-  background-image: url("@/assets/event.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-}
-
-.pa-flow-command {
-  background-image: url("@/assets/command.svg");
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 15px;
-  width: 15px;
-}
-
-path.link {
-  fill: none;
-  stroke: #ccc;
-  stroke-width: 2px;
-}
-</style>
diff --git a/src/Frontend/src/components/messages2/FlowDiagram/FlowDiagram.vue b/src/Frontend/src/components/messages2/FlowDiagram/FlowDiagram.vue
new file mode 100644
index 000000000..8432156d8
--- /dev/null
+++ b/src/Frontend/src/components/messages2/FlowDiagram/FlowDiagram.vue
@@ -0,0 +1,450 @@
+<script setup lang="ts">
+import { onMounted, ref, nextTick } from "vue";
+import { type DefaultEdge, MarkerType, type Node, useVueFlow, VueFlow, XYPosition } from "@vue-flow/core";
+import TimeSince from "../../TimeSince.vue";
+import routeLinks from "@/router/routeLinks.ts";
+import Message, { MessageIntent, MessageStatus, SagaInfo } from "@/resources/Message.ts";
+import { NServiceBusHeaders } from "@/resources/Header.ts";
+import { ControlButton, Controls } from "@vue-flow/controls";
+import { useMessageStore } from "@/stores/MessageStore.ts";
+import LoadingSpinner from "@/components/LoadingSpinner.vue";
+import { storeToRefs } from "pinia";
+import EndpointDetails from "@/resources/EndpointDetails.ts";
+import { hexToCSSFilter } from "hex-to-css-filter";
+import TextEllipses from "@/components/TextEllipses.vue";
+import { useLayout } from "@/components/messages2/FlowDiagram/useLayout.ts";
+
+enum MessageType {
+  Event = "Event message",
+  Timeout = "Timeout message",
+  Command = "Command message",
+}
+
+const store = useMessageStore();
+const { state } = storeToRefs(store);
+
+async function getConversation(conversationId: string) {
+  await store.loadConversation(conversationId);
+
+  return store.conversationData.data;
+}
+
+class SagaInvocation {
+  id: string;
+  sagaType: string;
+  isSagaCompleted: boolean;
+  isSagaInitiated: boolean;
+
+  constructor(saga: SagaInfo, message: Message) {
+    const sagaIdHeader = getHeaderByKey(message, NServiceBusHeaders.SagaId);
+    const originatedSagaIdHeader = getHeaderByKey(message, NServiceBusHeaders.OriginatingSagaId);
+    this.id = saga.saga_id;
+    this.sagaType = this.toName(saga.saga_type);
+    this.isSagaCompleted = saga.change_status === "Completed";
+    this.isSagaInitiated = sagaIdHeader === undefined && originatedSagaIdHeader !== undefined;
+  }
+
+  private toName(type: string) {
+    const clazz = type.split(",")[0];
+    let objectName = clazz.split(".").pop() ?? "";
+    objectName = objectName.replace("+", ".");
+
+    return objectName;
+  }
+}
+
+interface NodeData {
+  label: string;
+  timeSent: string;
+  messageId: string;
+  sendingEndpoint: EndpointDetails;
+  receivingEndpoint: EndpointDetails;
+  isError: boolean;
+  sagaInvocations: SagaInvocation[];
+  isPublished: boolean;
+  isTimeout: boolean;
+  isCommand: boolean;
+  isEvent: boolean;
+  message: Message;
+  type: MessageType;
+}
+
+class MessageNode implements Node<NodeData> {
+  readonly id: string;
+  readonly type: string;
+  readonly data: NodeData;
+  readonly position: XYPosition;
+  readonly draggable: boolean;
+
+  constructor(message: Message) {
+    this.id = message.id;
+    this.type = "message";
+    this.position = { x: 0, y: 0 };
+    this.draggable = false;
+
+    const isPublished = message.message_intent === MessageIntent.Publish;
+    const isTimeout = getHeaderByKey(message, NServiceBusHeaders.IsSagaTimeoutMessage)?.toLowerCase() === "true";
+    const sagas = message.invoked_sagas ?? [];
+
+    if (message.originates_from_saga) {
+      sagas.push(message.originates_from_saga);
+    }
+
+    this.data = {
+      label: message.message_type,
+      timeSent: message.time_sent,
+      messageId: message.message_id,
+      sendingEndpoint: message.sending_endpoint,
+      receivingEndpoint: message.receiving_endpoint,
+      isError: message.status !== MessageStatus.Successful && message.status !== MessageStatus.ResolvedSuccessfully,
+      sagaInvocations: sagas.map((saga) => new SagaInvocation(saga, message)),
+      isPublished,
+      isTimeout,
+      isEvent: isPublished && !isTimeout,
+      isCommand: !isPublished && !isTimeout,
+      message,
+      type: isPublished ? MessageType.Event : isTimeout ? MessageType.Timeout : MessageType.Command,
+    };
+  }
+}
+
+function constructNodes(messages: Message[]): Node<NodeData>[] {
+  const messageMap = new Map();
+
+  messages.forEach((message) => {
+    if (!messageMap.has(message.id)) {
+      messageMap.set(message.id, new MessageNode(message));
+    }
+  });
+
+  return Array.from(messageMap.values());
+}
+
+function getHeaderByKey(message: Message, key: NServiceBusHeaders) {
+  return message.headers.find((header) => header.key === key)?.value;
+}
+
+function constructEdges(nodes: Node<NodeData>[]): DefaultEdge[] {
+  const edges: DefaultEdge[] = [];
+
+  for (const node of nodes) {
+    const message = node.data?.message;
+    if (message === undefined) continue;
+
+    const relatedTo = getHeaderByKey(message, NServiceBusHeaders.RelatedTo);
+    if (!relatedTo && relatedTo !== message.message_id) {
+      continue;
+    }
+
+    let parentMessages = nodes.filter((n) => {
+      const m = n.data?.message;
+      if (m === undefined) return false;
+      return m.receiving_endpoint !== undefined && m.sending_endpoint !== undefined && m.message_id === relatedTo && m.receiving_endpoint.name === message.sending_endpoint.name;
+    });
+
+    if (parentMessages.length === 0) {
+      parentMessages = nodes.filter((n) => {
+        const m = n.data?.message;
+        if (m === undefined) return false;
+        return m.receiving_endpoint !== undefined && m.sending_endpoint !== undefined && m.message_id === relatedTo && m.message_intent !== MessageIntent.Publish;
+      });
+
+      if (parentMessages.length === 0) {
+        console.debug(`Fall back to match only on RelatedToMessageId for message with Id '${message.message_id}' matched but link could be invalid.`);
+      }
+    }
+
+    switch (parentMessages.length) {
+      case 0:
+        console.warn(
+          `No parent could be resolved for the message with Id '${message.message_id}' which has RelatedToMessageId set. This can happen if the parent has been purged due to retention expiration, an ServiceControl node to be unavailable, or because the parent message not been stored (yet).`
+        );
+        break;
+      case 1:
+        // Log nothing, this is what it should be
+        break;
+      default:
+        console.warn(`Multiple parents matched for message id '${message.message_id}' possibly due to more-than-once processing, linking to all as it is unknown which processing attempt generated the message.`);
+        break;
+    }
+
+    for (const parentMessage of parentMessages) {
+      edges.push(addConnection(parentMessage, node));
+    }
+  }
+
+  return edges;
+}
+
+function addConnection(parentMessage: Node<NodeData>, childMessage: Node<NodeData>): DefaultEdge {
+  return {
+    id: `${parentMessage.id}##${childMessage.id}`,
+    source: `${parentMessage.id}`,
+    target: `${childMessage.id}`,
+    markerEnd: MarkerType.ArrowClosed,
+    style: {
+      strokeDasharray: childMessage.data?.isPublished ? "5, 3" : "",
+      strokeWidth: 2,
+    },
+  };
+}
+
+const nodes = ref<Node[]>([]);
+const edges = ref<DefaultEdge[]>([]);
+const { layout } = useLayout();
+const { fitView } = useVueFlow();
+
+onMounted(async () => {
+  if (!state.value.data.conversation_id) return;
+
+  const messages = await getConversation(state.value.data.conversation_id);
+
+  nodes.value = constructNodes(messages);
+  edges.value = constructEdges(nodes.value);
+});
+
+async function layoutGraph() {
+  nodes.value = layout(nodes.value, edges.value, showAddress.value);
+
+  await nextTick(() => {
+    if (store.state.data.id) {
+      fitView({ nodes: [store.state.data.id], maxZoom: 0.9 });
+    }
+  });
+}
+
+const showAddress = ref(true);
+
+async function toggleAddress() {
+  showAddress.value = !showAddress.value;
+  await layoutGraph();
+}
+
+const blackColor = hexToCSSFilter("#000000").filter;
+const greenColor = hexToCSSFilter("#1E5E3C").filter;
+const errorColor = hexToCSSFilter("#be514a").filter;
+</script>
+
+<template>
+  <div v-if="store.conversationData.failed_to_load" class="alert alert-info">FlowDiagram data is unavailable.</div>
+  <LoadingSpinner v-else-if="store.conversationData.loading" />
+  <div v-else id="tree-container">
+    <VueFlow :nodes="nodes" :edges="edges" :min-zoom="0.1" :max-zoom="1.2" :only-render-visible-elements="true" @nodes-initialized="layoutGraph">
+      <Controls :show-interactive="false" position="top-left" class="controls">
+        <ControlButton v-tippy="showAddress ? `Hide endpoints` : `Show endpoints`" @click="toggleAddress">
+          <i class="fa pa-flow-endpoint" :style="{ filter: showAddress ? greenColor : blackColor }"></i>
+        </ControlButton>
+      </Controls>
+      <template #node-message="{ id, data }: { id: string; data: NodeData }">
+        <div v-if="showAddress">
+          <TextEllipses class="address" :text="`${data.sendingEndpoint.name}@${data.sendingEndpoint.host}`" />
+        </div>
+        <div class="node" :class="{ error: data.isError, 'current-message': id === store.state.data.id }">
+          <div class="node-text">
+            <i class="fa" :class="{ 'pa-flow-timeout': data.isTimeout, 'pa-flow-command': data.isCommand, 'pa-flow-event': data.isEvent }" v-tippy="data.type" />
+            <div class="lead">
+              <strong>
+                <RouterLink v-if="data.isError" :to="{ path: routeLinks.messages.failedMessage.link(id) }"><TextEllipses style="width: 204px" :text="data.label" ellipses-style="LeftSide" /></RouterLink>
+                <RouterLink v-else :to="{ path: routeLinks.messages.successMessage.link(data.messageId, id) }"><TextEllipses style="width: 204px" :text="data.label" ellipses-style="LeftSide" /></RouterLink>
+              </strong>
+            </div>
+            <i v-if="data.isError" class="fa pa-flow-failed" :style="id !== store.state.data.id ? { filter: errorColor } : {}" />
+            <div class="time-sent">
+              <time-since class="time-since" :date-utc="data.timeSent" />
+            </div>
+            <div class="sagas" v-if="data.sagaInvocations.length > 0">
+              <div class="saga" v-for="saga in data.sagaInvocations" :key="saga.id">
+                <i
+                  class="fa"
+                  v-tippy="saga.isSagaInitiated ? 'Message originated from Saga' : !saga.isSagaInitiated && saga.isSagaCompleted ? 'Saga Completed' : 'Saga Initiated / Updated'"
+                  :class="{ 'pa-flow-saga-initiated': saga.isSagaInitiated, 'pa-flow-saga-completed': !saga.isSagaInitiated && saga.isSagaCompleted, 'pa-flow-saga-trigger': !saga.isSagaInitiated && !saga.isSagaCompleted }"
+                />
+                <div class="sagaName"><TextEllipses style="width: 182px" :text="saga.sagaType" ellipses-style="LeftSide" /></div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div v-if="showAddress">
+          <TextEllipses class="address" :text="`${data.receivingEndpoint.name}@${data.receivingEndpoint.host}`" />
+        </div>
+      </template>
+    </VueFlow>
+  </div>
+</template>
+
+<style>
+@import "@vue-flow/core/dist/style.css";
+@import "@vue-flow/core/dist/theme-default.css";
+@import "@vue-flow/controls/dist/style.css";
+</style>
+
+<style scoped>
+@import "../../list.css";
+
+.controls {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+#tree-container {
+  width: 92vw;
+  height: 70vh;
+}
+
+.sagas {
+  background-color: #333333;
+}
+
+.saga {
+  display: flex;
+}
+
+.sagaName {
+  color: #e6e6e6;
+}
+.node {
+  --vf-handle: var(--vf-node-color, #1a192b);
+  --vf-box-shadow: var(--vf-node-color, #1a192b);
+  background: var(--vf-node-bg);
+  border-color: var(--vf-node-color, #1a192b);
+  border-radius: 3px;
+  font-size: 12px;
+  border-width: 1px;
+  border-style: solid;
+  color: var(--vf-node-text);
+  text-align: left;
+}
+
+.node {
+  background-color: #fff;
+  border-color: #cccbcc;
+  border-width: 3px;
+}
+
+.node .error {
+  border-color: #be514a;
+}
+
+.node text {
+  font: 12px sans-serif;
+}
+
+.node .time-sent .time-since {
+  margin-left: 20px;
+  padding-top: 0;
+  color: #262727;
+  text-transform: capitalize;
+}
+
+.node-text {
+  padding: 3px 8px 1px;
+}
+
+.node-text i {
+  display: inline-block;
+  position: relative;
+  margin-right: 5px;
+}
+
+.node-text .lead {
+  display: inline-block;
+  position: relative;
+  top: 4px;
+}
+
+.address {
+  color: #777f7f;
+  font-size: 0.8em;
+  width: 264px;
+}
+
+.current-message {
+  border-color: #cccbcc;
+  background-color: #cccbcc !important;
+}
+
+.current-message.error {
+  border-color: #be514a;
+  background-color: #be514a !important;
+}
+
+.error {
+  border-color: #be514a;
+}
+
+.current-message.error .node-text a:hover {
+  cursor: text;
+  text-decoration: none;
+}
+
+.node-text a {
+  color: #000;
+}
+
+.error .node-text a:hover {
+  text-decoration: underline;
+}
+
+.pa-flow-endpoint {
+  background-image: url("@/assets/endpoint.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+
+.pa-flow-failed {
+  background-image: url("@/assets/failed-msg.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+
+.pa-flow-saga-completed {
+  background-image: url("@/assets/saga-completed.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+.pa-flow-saga-initiated {
+  background-image: url("@/assets/saga-initiated.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+.pa-flow-saga-trigger {
+  background-image: url("@/assets/saga-trigger.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+
+.pa-flow-timeout {
+  background-image: url("@/assets/timeout.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+
+.pa-flow-event {
+  background-image: url("@/assets/event.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+
+.pa-flow-command {
+  background-image: url("@/assets/command.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  height: 15px;
+  width: 15px;
+}
+</style>
diff --git a/src/Frontend/src/components/messages2/FlowDiagram/useLayout.ts b/src/Frontend/src/components/messages2/FlowDiagram/useLayout.ts
new file mode 100644
index 000000000..7f58dd711
--- /dev/null
+++ b/src/Frontend/src/components/messages2/FlowDiagram/useLayout.ts
@@ -0,0 +1,48 @@
+import dagre from "@dagrejs/dagre";
+import { DefaultEdge, Node, Position, useVueFlow } from "@vue-flow/core";
+import { ref } from "vue";
+
+export function useLayout() {
+  const { findNode } = useVueFlow();
+
+  const graph = ref(new dagre.graphlib.Graph());
+
+  function layout(nodes: Node[], edges: DefaultEdge[], showAddress: boolean) {
+    // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
+    const dagreGraph = new dagre.graphlib.Graph();
+
+    graph.value = dagreGraph;
+
+    dagreGraph.setDefaultEdgeLabel(() => ({}));
+
+    const isHorizontal = false;
+    dagreGraph.setGraph({ rankdir: "TB" });
+
+    for (const node of nodes) {
+      const graphNode = findNode(node.id);
+      if (graphNode === undefined) continue;
+
+      dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 250, height: (graphNode.dimensions.height || 55) + (showAddress ? 40 : 0) });
+    }
+
+    for (const edge of edges) {
+      dagreGraph.setEdge(edge.source, edge.target);
+    }
+
+    dagre.layout(dagreGraph);
+
+    // set nodes with updated positions
+    return nodes.map((node) => {
+      const nodeWithPosition = dagreGraph.node(node.id);
+
+      return {
+        ...node,
+        targetPosition: isHorizontal ? Position.Left : Position.Top,
+        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
+        position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
+      };
+    });
+  }
+
+  return { graph, layout };
+}
diff --git a/src/Frontend/src/components/messages2/MessageView.vue b/src/Frontend/src/components/messages2/MessageView.vue
index 9c7ce73c4..c22b956fd 100644
--- a/src/Frontend/src/components/messages2/MessageView.vue
+++ b/src/Frontend/src/components/messages2/MessageView.vue
@@ -3,7 +3,7 @@ import { computed, watch } from "vue";
 import { RouterLink, useRoute } from "vue-router";
 import NoData from "../NoData.vue";
 import TimeSince from "../TimeSince.vue";
-import FlowDiagram from "./FlowDiagram.vue";
+import FlowDiagram from "./FlowDiagram/FlowDiagram.vue";
 import SequenceDiagram from "./SequenceDiagram.vue";
 import routeLinks from "@/router/routeLinks";
 import { useIsMassTransitConnected } from "@/composables/useIsMassTransitConnected";
diff --git a/src/Frontend/src/resources/Message.ts b/src/Frontend/src/resources/Message.ts
index 7bc049462..ed1386960 100644
--- a/src/Frontend/src/resources/Message.ts
+++ b/src/Frontend/src/resources/Message.ts
@@ -42,7 +42,7 @@ export enum MessageIntent {
   Init = "init",
 }
 
-interface SagaInfo {
+export interface SagaInfo {
   change_status?: string;
   saga_type: string;
   saga_id: string;