Repository: keephq/keep
Pull request: feat: workflow versioning
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #4326 

## Demo

https://github.com/user-attachments/assets/1b49c9c4-6aab-43ca-9b71-c1632ca4f41f



Problem statement:
[➕ Feature]: workflow versioning
first stage proposal:
1.  add revision to the primary key constraint
2. for each new revision create new row in workflow table with incremented revision and same id
3. in api, return latest revision for id
4. add endpoints to get revisions, e.g. `/workflows/{id}/revision/{revision}`

Hints from issue discussion:

Commit message: wip: workflowversion table

Patch to review:
diff --git a/keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx b/keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx
index 304140729a..e110f40082 100644
--- a/keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx
+++ b/keep-ui/app/(keep)/extraction/[rule_id]/executions/[execution_id]/page.tsx
@@ -4,11 +4,11 @@ import { use } from "react";
 
 import { Card, Title, Badge, Icon, Subtitle } from "@tremor/react";
 import { LogViewer } from "@/components/LogViewer";
-import { getIcon } from "@/app/(keep)/workflows/[workflow_id]/workflow-execution-table";
 import { useEnrichmentEvent } from "@/utils/hooks/useEnrichmentEvents";
 import { Link } from "@/components/ui";
 import { ArrowRightIcon } from "@heroicons/react/16/solid";
 import { useExtractions } from "@/utils/hooks/useExtractionRules";
+import { getIconForStatusString } from "@/shared/ui";
 
 export default function ExtractionExecutionDetailsPage(props: {
   params: Promise<{ rule_id: string; execution_id: string }>;
@@ -51,7 +51,7 @@ export default function ExtractionExecutionDetailsPage(props: {
           <Title>Execution Details</Title>
           <div className="flex items-center space-x-2">
             <span>Status:</span>
-            {getIcon(execution.enrichment_event.status)}
+            {getIconForStatusString(execution.enrichment_event.status)}
           </div>
         </div>
       </div>
diff --git a/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-sidebar.tsx b/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-sidebar.tsx
index b405b1d805..345249d1a9 100644
--- a/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-sidebar.tsx
+++ b/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-sidebar.tsx
@@ -2,16 +2,16 @@ import { Fragment } from "react";
 import { Dialog, Transition } from "@headlessui/react";
 import { Text, Button, TextInput, Badge, Title, Card } from "@tremor/react";
 import { IoMdClose } from "react-icons/io";
-import {
-  getIcon,
-  getTriggerIcon,
-  extractTriggerValue,
-} from "@/app/(keep)/workflows/[workflow_id]/workflow-execution-table";
 import {
   isWorkflowExecution,
   WorkflowExecutionDetail,
 } from "@/shared/api/workflow-executions";
 import { useWorkflowExecutionDetail } from "@/entities/workflow-executions/model/useWorkflowExecutionDetail";
+import {
+  extractTriggerValue,
+  getTriggerIcon,
+} from "@/entities/workflows/lib/ui-utils";
+import { getIconForStatusString } from "@/shared/ui";
 interface IncidentWorkflowSidebarProps {
   isOpen: boolean;
   toggle: VoidFunction;
@@ -97,7 +97,7 @@ const IncidentWorkflowSidebar: React.FC<IncidentWorkflowSidebarProps> = ({
                       Status
                     </Text>
                     <div className="flex items-center">
-                      {getIcon(selectedExecution.status)}
+                      {getIconForStatusString(selectedExecution.status)}
                       <span className="ml-2 capitalize">
                         {selectedExecution.status}
                       </span>
diff --git a/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-table.tsx b/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-table.tsx
index dd0cd7c99a..686d25fa93 100644
--- a/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-table.tsx
+++ b/keep-ui/app/(keep)/incidents/[id]/workflows/incident-workflow-table.tsx
@@ -20,12 +20,6 @@ import {
   TableHeaderCell,
   TableRow,
 } from "@tremor/react";
-import {
-  extractTriggerDetails,
-  extractTriggerValue,
-  getIcon,
-  getTriggerIcon,
-} from "@/app/(keep)/workflows/[workflow_id]/workflow-execution-table";
 import { WorkflowExecutionDetail } from "@/shared/api/workflow-executions";
 import { useEffect, useState } from "react";
 import Skeleton from "react-loading-skeleton";
@@ -33,7 +27,12 @@ import "react-loading-skeleton/dist/skeleton.css";
 import { useIncidentWorkflowExecutions } from "utils/hooks/useIncidents";
 import { IncidentWorkflowsEmptyState } from "./incident-workflow-empty";
 import IncidentWorkflowSidebar from "./incident-workflow-sidebar";
-import { TablePagination } from "@/shared/ui";
+import { TablePagination, getIconForStatusString } from "@/shared/ui";
+import {
+  extractTriggerDetails,
+  extractTriggerValue,
+  getTriggerIcon,
+} from "@/entities/workflows/lib/ui-utils";
 
 interface Props {
   incident: IncidentDto;
@@ -106,7 +105,7 @@ export default function IncidentWorkflowTable({ incident }: Props) {
     }),
     columnHelper.accessor("status", {
       header: "Status",
-      cell: (info) => getIcon(info.getValue()),
+      cell: (info) => getIconForStatusString(info.getValue()),
     }),
     columnHelper.accessor("started", {
       header: "Start Time",
diff --git a/keep-ui/app/(keep)/mapping/[rule_id]/executions/[execution_id]/page.tsx b/keep-ui/app/(keep)/mapping/[rule_id]/executions/[execution_id]/page.tsx
index 36f988884b..2439936b2b 100644
--- a/keep-ui/app/(keep)/mapping/[rule_id]/executions/[execution_id]/page.tsx
+++ b/keep-ui/app/(keep)/mapping/[rule_id]/executions/[execution_id]/page.tsx
@@ -4,11 +4,11 @@ import { use } from "react";
 
 import { Card, Title, Badge, Icon, Subtitle } from "@tremor/react";
 import { LogViewer } from "@/components/LogViewer";
-import { getIcon } from "@/app/(keep)/workflows/[workflow_id]/workflow-execution-table";
 import { useEnrichmentEvent } from "@/utils/hooks/useEnrichmentEvents";
 import { Link } from "@/components/ui";
 import { ArrowRightIcon } from "@heroicons/react/16/solid";
 import { useMappings } from "@/utils/hooks/useMappingRules";
+import { getIconForStatusString } from "@/shared/ui";
 
 export default function MappingExecutionDetailsPage(props: {
   params: Promise<{ rule_id: string; execution_id: string }>;
@@ -49,7 +49,7 @@ export default function MappingExecutionDetailsPage(props: {
           <Title>Execution Details</Title>
           <div className="flex items-center space-x-2">
             <span>Status:</span>
-            {getIcon(execution.enrichment_event.status)}
+            {getIconForStatusString(execution.enrichment_event.status)}
           </div>
         </div>
       </div>
diff --git a/keep-ui/app/(keep)/settings/auth/users-table.tsx b/keep-ui/app/(keep)/settings/auth/users-table.tsx
index deacb89a4a..7ce2acb77c 100644
--- a/keep-ui/app/(keep)/settings/auth/users-table.tsx
+++ b/keep-ui/app/(keep)/settings/auth/users-table.tsx
@@ -14,7 +14,7 @@ import Image from "next/image";
 import { TrashIcon } from "@heroicons/react/24/outline";
 import { AuthType } from "utils/authenticationType";
 import { User } from "@/app/(keep)/settings/models";
-import { getInitials } from "@/components/navbar/UserAvatar";
+import UserAvatar, { getInitials } from "@/components/navbar/UserAvatar";
 
 interface UsersTableProps {
   users: User[];
@@ -41,7 +41,6 @@ export function UsersTable({
     <Table>
       <TableHead>
         <TableRow className="border-b border-tremor-border dark:border-dark-tremor-border">
-          <TableHeaderCell className="w-1/24">{/** Image */}</TableHeaderCell>
           <TableHeaderCell className="w-3/12">
             {authType === AuthType.AUTH0 || authType === AuthType.KEYCLOAK
               ? "Email"
@@ -67,26 +66,17 @@ export function UsersTable({
             `}
             onClick={() => !isDisabled && onRowClick && onRowClick(user)}
           >
-            <TableCell>
-              {user.picture ? (
-                <Image
-                  className="rounded-full w-7 h-7 inline"
-                  src={user.picture}
-                  alt="user avatar"
-                  width={28}
-                  height={28}
-                />
-              ) : (
-                <span className="relative inline-flex items-center justify-center w-7 h-7 overflow-hidden bg-orange-400 rounded-full dark:bg-gray-600">
-                  <span className="font-medium text-white text-xs">
-                    {getInitials(user.name ?? user.email)}
-                  </span>
-                </span>
-              )}
-            </TableCell>
             <TableCell className="w-3/12">
               <div className="flex items-center justify-between">
-                <Text className="truncate">{user.email}</Text>
+                <div className="flex items-center gap-2">
+                  <UserAvatar
+                    image={user.picture}
+                    name={user.name ?? user.email}
+                    email={user.email}
+                    size="sm"
+                  />
+                  <Text className="truncate">{user.email}</Text>
+                </div>
                 <div className="ml-2">
                   {user.ldap && <Badge color="orange">LDAP</Badge>}
                 </div>
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/versions/[revision]/page.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/versions/[revision]/page.tsx
new file mode 100644
index 0000000000..87c84a3ba9
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/versions/[revision]/page.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useWorkflowDetail } from "@/entities/workflows/model";
+import { WorkflowYAMLEditor } from "@/shared/ui";
+import { Card } from "@tremor/react";
+
+export default function WorkflowVersionPage() {
+  const { workflow_id, revision } = useParams();
+
+  const { workflow } = useWorkflowDetail(
+    workflow_id as string,
+    Number(revision)
+  );
+
+  return (
+    <div className="flex flex-col gap-4">
+      <h1>Workflow Revision {revision}</h1>
+      <Card className="h-[calc(100vh-12rem)] p-0">
+        <WorkflowYAMLEditor
+          value={workflow?.workflow_raw ?? ""}
+          readOnly={true}
+        />
+      </Card>
+    </div>
+  );
+}
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-breadcrumbs.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-breadcrumbs.tsx
index 258a04b28f..300faf19ef 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-breadcrumbs.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-breadcrumbs.tsx
@@ -12,15 +12,28 @@ export function WorkflowBreadcrumbs({ workflowId }: { workflowId: string }) {
   return (
     <Subtitle className="text-sm">
       <Link href="/workflows">All Workflows</Link>{" "}
-      <Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
       {clientParams.workflow_execution_id ? (
         <>
+          <Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
           <Link href={`/workflows/${workflowId}`}>Workflow Details</Link>
           <Icon icon={ArrowRightIcon} color="gray" size="xs" /> Workflow
           Execution Details
         </>
       ) : (
-        "Workflow Details"
+        <>
+          <Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
+          <Link href={`/workflows/${workflowId}`}>Workflow Details</Link>
+        </>
+      )}
+      {clientParams.revision && (
+        <>
+          <Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
+          <Link
+            href={`/workflows/${workflowId}/versions/${clientParams.revision}`}
+          >
+            Workflow Revision {clientParams.revision}
+          </Link>
+        </>
       )}
     </Subtitle>
   );
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-header.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-header.tsx
index 3e26f9e2a8..b18ffc2440 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-header.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-header.tsx
@@ -15,7 +15,7 @@ export default function WorkflowDetailHeader({
   workflowId: string;
   initialData?: Workflow;
 }) {
-  const { workflow, isLoading, error } = useWorkflowDetail(workflow_id, {
+  const { workflow, error } = useWorkflowDetail(workflow_id, null, {
     fallbackData: initialData,
   });
 
@@ -26,7 +26,6 @@ export default function WorkflowDetailHeader({
     getManualInputModalProps,
     isRunButtonDisabled,
     message,
-    hasInputs,
   } = useWorkflowRun(workflow as Workflow);
 
   if (error) {
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx
index 734b651c55..7c20dba1ac 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx
@@ -27,6 +27,18 @@ import { useRouter, useSearchParams } from "next/navigation";
 import { useWorkflowDetail } from "@/entities/workflows/model/useWorkflowDetail";
 import { WorkflowYAMLEditorStandalone } from "@/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone";
 import { getOrderedWorkflowYamlString } from "@/entities/workflows/lib/yaml-utils";
+import { PiClockCounterClockwise } from "react-icons/pi";
+import { WorkflowVersions } from "./workflow-versions";
+
+const TABS_KEYS = ["overview", "builder", "yaml", "versions", "secrets"];
+
+function getTabIndex(tabKey: string) {
+  const index = TABS_KEYS.indexOf(tabKey);
+  if (index !== -1) {
+    return index;
+  }
+  return 0;
+}
 
 export default function WorkflowDetailPage({
   params,
@@ -36,27 +48,25 @@ export default function WorkflowDetailPage({
   initialData?: Workflow;
 }) {
   const { data: configData } = useConfig();
-  const [tabIndex, setTabIndex] = useState(0);
   const searchParams = useSearchParams();
+  const [tabIndex, setTabIndex] = useState(
+    getTabIndex(searchParams.get("tab") ?? "")
+  );
   const router = useRouter();
 
   // Set initial tab based on URL query param
   useEffect(() => {
     const tab = searchParams.get("tab");
-    if (tab === "yaml") {
-      setTabIndex(2);
-    } else if (tab === "builder") {
-      setTabIndex(1);
-    } else if (tab === "secrets") {
-      setTabIndex(3);
-    } else {
-      setTabIndex(0);
-    }
+    setTabIndex(getTabIndex(tab ?? ""));
   }, [searchParams]);
 
-  const { workflow, isLoading, error } = useWorkflowDetail(params.workflow_id, {
-    fallbackData: initialData,
-  });
+  const { workflow, isLoading, error } = useWorkflowDetail(
+    params.workflow_id,
+    null,
+    {
+      fallbackData: initialData,
+    }
+  );
 
   const docsUrl = configData?.KEEP_DOCS_URL || "https://docs.keephq.dev";
 
@@ -67,19 +77,9 @@ export default function WorkflowDetailPage({
   const handleTabChange = (index: number) => {
     setTabIndex(index);
     const basePath = `/workflows/${params.workflow_id}`;
-    switch (index) {
-      case 0:
-        router.push(basePath);
-        break;
-      case 1:
-        router.push(`${basePath}?tab=builder`);
-        break;
-      case 2:
-        router.push(`${basePath}?tab=yaml`);
-        break;
-      case 3:
-        router.push(`${basePath}?tab=secrets`);
-        break;
+    const tabKey = TABS_KEYS[index];
+    if (tabKey) {
+      router.push(`${basePath}?tab=${tabKey}`);
     }
   };
 
@@ -90,6 +90,7 @@ export default function WorkflowDetailPage({
           <Tab icon={AiOutlineSwap}>Overview</Tab>
           <Tab icon={WrenchIcon}>Builder</Tab>
           <Tab icon={CodeBracketIcon}>YAML Definition</Tab>
+          <Tab icon={PiClockCounterClockwise}>Versions</Tab>
           <Tab icon={KeyIcon}>Secrets</Tab>
           <TabNavigationLink
             href="https://www.youtube.com/@keepalerting"
@@ -107,13 +108,13 @@ export default function WorkflowDetailPage({
           </TabNavigationLink>
         </TabList>
         <TabPanels>
-          <TabPanel>
+          <TabPanel id="overview">
             <WorkflowOverview
               workflow={workflow ?? null}
               workflow_id={params.workflow_id}
             />
           </TabPanel>
-          <TabPanel>
+          <TabPanel id="builder">
             {!workflow ? (
               <Skeleton className="w-full h-full" />
             ) : (
@@ -125,7 +126,7 @@ export default function WorkflowDetailPage({
               </Card>
             )}
           </TabPanel>
-          <TabPanel>
+          <TabPanel id="yaml">
             {!workflow || !workflow.workflow_raw ? (
               <Skeleton className="w-full h-full" />
             ) : (
@@ -140,7 +141,13 @@ export default function WorkflowDetailPage({
               </Card>
             )}
           </TabPanel>
-          <TabPanel>
+          <TabPanel id="versions">
+            <WorkflowVersions
+              workflowId={params.workflow_id}
+              currentRevision={workflow?.revision ?? null}
+            />
+          </TabPanel>
+          <TabPanel id="secrets">
             <WorkflowSecrets workflowId={params.workflow_id} />
           </TabPanel>
         </TabPanels>
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx
deleted file mode 100644
index 9108cfc9bf..0000000000
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx
+++ /dev/null
@@ -1,460 +0,0 @@
-import {
-  createColumnHelper,
-  DisplayColumnDef,
-  Row,
-} from "@tanstack/react-table";
-import {
-  PaginatedWorkflowExecutionDto,
-  WorkflowExecutionDetail,
-} from "@/shared/api/workflow-executions";
-import { GenericTable } from "@/components/table/GenericTable";
-import Link from "next/link";
-import { Dispatch, Fragment, SetStateAction } from "react";
-import Image from "next/image";
-import {
-  CheckCircleIcon,
-  EllipsisHorizontalIcon,
-  XCircleIcon,
-  NoSymbolIcon,
-} from "@heroicons/react/20/solid";
-import TimeAgo, { Formatter, Suffix, Unit } from "react-timeago";
-import { formatDistanceToNowStrict } from "date-fns";
-import { Menu, Transition } from "@headlessui/react";
-import { Badge, Icon } from "@tremor/react";
-import { useRouter } from "next/navigation";
-import {
-  ClockIcon,
-  CursorArrowRaysIcon,
-  QuestionMarkCircleIcon,
-} from "@heroicons/react/24/outline";
-
-interface Pagination {
-  limit: number;
-  offset: number;
-}
-interface Props {
-  executions: PaginatedWorkflowExecutionDto;
-  setPagination: Dispatch<SetStateAction<Pagination>>;
-}
-
-function ExecutionRowMenu({ row }: { row: Row<WorkflowExecutionDetail> }) {
-  const stopPropagation = (e: React.MouseEvent<HTMLButtonElement>) => {
-    e.stopPropagation();
-  };
-  return (
-    <Menu as="div" className="realtive inline-block text-left">
-      <div>
-        <Menu.Button
-          className="inline-flex w-full justify-center rounded-md text-sm"
-          onClick={stopPropagation}
-        >
-          <Icon
-            size="sm"
-            icon={EllipsisHorizontalIcon}
-            className="hover:bg-gray-100 w-8 h-8"
-            color="gray"
-          />
-        </Menu.Button>
-      </div>
-      <Transition
-        as={Fragment}
-        enter="transition ease-out duration-100"
-        enterFrom="transform opacity-0 scale-95"
-        enterTo="transform opacity-100 scale-100"
-        leave="transition ease-in duration-75"
-        leaveFrom="transform opacity-100 scale-100"
-        leaveTo="transform opacity-0 scale-95"
-      >
-        <Menu.Items className="absolute z-20 right-0 mt-2 w-36 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
-          {/* <Menu.Items className="absolute right-0 z-20 mb-2 w-36 divide-y  origin-middle-left divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> */}
-          <div className="px-1 py-1">
-            <Menu.Item>
-              {({ active }) => (
-                <Link
-                  className="flex items-center p-2"
-                  href={`/workflows/${row.original.workflow_id}/runs/${row.original.id}`}
-                  passHref
-                >
-                  View Logs
-                </Link>
-              )}
-            </Menu.Item>
-          </div>
-        </Menu.Items>
-      </Transition>
-    </Menu>
-  );
-}
-
-export function getIcon(status: string) {
-  let icon = (
-    <Image
-      className="animate-bounce size-6 cover"
-      src="/keep.svg"
-      alt="loading"
-      width={40}
-      height={40}
-    />
-  );
-  switch (status) {
-    case "success":
-      icon = <CheckCircleIcon className="size-6 cover text-green-500" />;
-      break;
-    case "skipped":
-      icon = (
-        <NoSymbolIcon className="size-6 cover text-slate-500" title="Skipped" />
-      );
-      break;
-    case "failed":
-    case "fail":
-    case "failure":
-    case "error":
-    case "timeout":
-      icon = <XCircleIcon className="size-6 cover text-red-500" />;
-      break;
-    case "in_progress":
-      icon = <div className="loader"></div>;
-      break;
-    default:
-      icon = <div className="loader"></div>;
-  }
-  return icon;
-}
-
-const KeepIncidentIcon = () => (
-  <Image
-    src="/keep.png"
-    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
-    width={16}
-    height={16}
-    alt="Keep Incident"
-  />
-);
-
-const KeepAlertIcon = () => (
-  <Image
-    src="/keep.png"
-    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
-    width={16}
-    height={16}
-    alt="Keep Alert"
-  />
-);
-
-export function getTriggerIcon(triggered_by: string) {
-  switch (triggered_by) {
-    case "manual":
-      return CursorArrowRaysIcon;
-    case "interval":
-      return ClockIcon;
-    case "alert":
-      return KeepAlertIcon;
-    case "incident":
-      return KeepIncidentIcon;
-    default:
-      return QuestionMarkCircleIcon;
-  }
-}
-
-export function getTriggerIconV2(type: string, details: string) {
-  switch (type) {
-    case "manual":
-      return CursorArrowRaysIcon;
-    case "interval":
-      return ClockIcon;
-    case "alert":
-      return KeepAlertIcon;
-    case "incident":
-      return KeepIncidentIcon;
-    default:
-      return QuestionMarkCircleIcon;
-  }
-}
-
-export function extractTriggerValue(triggered_by: string | undefined): string {
-  if (!triggered_by) return "others";
-
-  if (triggered_by.startsWith("scheduler")) {
-    return "interval";
-  } else if (triggered_by.startsWith("type:alert")) {
-    return "alert";
-  } else if (triggered_by.startsWith("manually")) {
-    return triggered_by;
-  } else if (triggered_by.startsWith("type:incident:")) {
-    const incidentType = triggered_by
-      .substring("type:incident:".length)
-      .split(" ")[0];
-    return `incident ${incidentType}`;
-  } else {
-    return "others";
-  }
-}
-
-export function extractTriggerType(
-  triggered_by: string | undefined
-): "interval" | "alert" | "manual" | "incident" | "unknown" {
-  if (!triggered_by) {
-    return "unknown";
-  }
-
-  if (triggered_by.startsWith("scheduler")) {
-    return "interval";
-  } else if (triggered_by.startsWith("type:alert")) {
-    return "alert";
-  } else if (triggered_by.startsWith("manually")) {
-    return "manual";
-  } else if (triggered_by.startsWith("type:incident:")) {
-    return "incident";
-  } else {
-    return "unknown";
-  }
-}
-
-export function extractTriggerDetails(
-  triggered_by: string | undefined
-): string[] {
-  if (!triggered_by) {
-    return [];
-  }
-
-  let details: string;
-  if (triggered_by.startsWith("scheduler")) {
-    details = triggered_by.substring("scheduler".length).trim();
-  } else if (triggered_by.startsWith("type:alert")) {
-    details = triggered_by.substring("type:alert".length).trim();
-  } else if (triggered_by.startsWith("manual")) {
-    details = triggered_by.substring("manual".length).trim();
-  } else if (triggered_by.startsWith("type:incident:")) {
-    // Handle 'type:incident:{some operator}' by removing the operator
-    details = triggered_by.substring("type:incident:".length).trim();
-    const firstSpaceIndex = details.indexOf(" ");
-    if (firstSpaceIndex > -1) {
-      details = details.substring(firstSpaceIndex).trim();
-    } else {
-      details = "";
-    }
-  } else {
-    details = triggered_by;
-  }
-
-  // Split the string into key-value pairs, where values may contain spaces
-  const regex = /\b(\w+:[^:]+?)(?=\s\w+:|$)/g;
-  const matches = details.match(regex);
-
-  return matches ?? [];
-}
-
-type TriggerDetails = {
-  type: "manual" | "interval" | "alert" | "incident" | "unknown";
-  details: Record<string, string>;
-};
-
-export function extractTriggerDetailsV2(
-  triggered_by: string | undefined
-): TriggerDetails {
-  if (!triggered_by) {
-    return { type: "unknown", details: {} };
-  }
-
-  let type: TriggerDetails["type"] = extractTriggerType(triggered_by);
-  let details: string;
-  if (triggered_by.startsWith("scheduler")) {
-    // details = triggered_by.substring("scheduler".length).trim();
-    details = "scheduler";
-  } else if (triggered_by.startsWith("type:alert")) {
-    details = triggered_by.substring("type:alert".length).trim();
-  } else if (triggered_by.startsWith("manually by")) {
-    details = "user:" + triggered_by.substring("manually by".length).trim();
-  } else if (triggered_by.startsWith("type:incident:")) {
-    // Handle 'type:incident:{some operator}' by removing the operator
-    details = triggered_by.substring("type:incident:".length).trim();
-    const firstSpaceIndex = details.indexOf(" ");
-    if (firstSpaceIndex > -1) {
-      details = details.substring(firstSpaceIndex).trim();
-    } else {
-      details = "";
-    }
-  } else {
-    details = triggered_by;
-  }
-
-  // Split the string into key-value pairs, where values may contain spaces
-  const regex = /\b(\w+:[^:]+?)(?=\s\w+:|$)/g;
-  const matches = details.match(regex);
-
-  return {
-    type,
-    details: matches
-      ? Object.fromEntries(
-          matches.map((match) => {
-            const [key, value] = match.split(":");
-            return [key, value];
-          })
-        )
-      : {},
-  };
-}
-
-export function ExecutionTable({ executions, setPagination }: Props) {
-  const columnHelper = createColumnHelper<WorkflowExecutionDetail>();
-  const router = useRouter();
-
-  const columns = [
-    columnHelper.display({
-      id: "status",
-      header: "Status",
-      cell: ({ row }) => {
-        const status = row.original.status;
-        return <div>{getIcon(status)}</div>;
-      },
-    }),
-    columnHelper.display({
-      id: "id",
-      header: "Execution ID",
-      cell: ({ row }) => {
-        const status = row.original.status;
-        const isError = ["timeout", "error", "fail", "failed"].includes(status);
-        return (
-          <div className={`${isError ? "text-red-500" : ""}`}>
-            {row.original.id}
-          </div>
-        );
-      },
-    }),
-    columnHelper.display({
-      id: "triggered_by",
-      header: "Triggered by",
-      cell: ({ row }) => {
-        const triggered_by = row.original.triggered_by;
-        const { type, details } = extractTriggerDetailsV2(triggered_by);
-
-        let detailsContent: React.ReactNode = type as string;
-
-        if (type === "incident") {
-          detailsContent = (
-            <Link
-              href={`/incidents/${details.id}`}
-              target="_blank"
-              onClick={(e) => e.stopPropagation()}
-            >
-              {details.name}
-            </Link>
-          );
-        }
-        if (type === "alert") {
-          detailsContent = (
-            <Link
-              href={`/alerts/feed/?cel=${encodeURIComponent(
-                `id=="${details.id}"`
-              )}`}
-              onClick={(e) => e.stopPropagation()}
-            >
-              Alert &quot;{details.name}&quot;
-            </Link>
-          );
-        }
-        if (type === "manual") {
-          detailsContent = `Manually by ${details.user}`;
-        }
-        if (type === "interval") {
-          detailsContent = `Interval`;
-        }
-        return (
-          <>
-            <Badge
-              color="gray"
-              tooltip={Object.entries(details)
-                .map(([key, value]) => `${key}: ${value}`)
-                .join(", ")}
-              icon={getTriggerIcon(type)}
-            >
-              {detailsContent}
-            </Badge>
-          </>
-        );
-      },
-    }),
-    columnHelper.display({
-      id: "execution_time",
-      header: "Execution Duration",
-      cell: ({ row }) => {
-        const customFormatter = (seconds: number | null) => {
-          if (seconds === undefined || seconds === null) {
-            return "";
-          }
-
-          const hours = Math.floor(seconds / 3600);
-          const minutes = Math.floor((seconds % 3600) / 60);
-          const remainingSeconds = seconds % 60;
-
-          if (hours > 0) {
-            return `${hours} hr ${minutes}m ${remainingSeconds}s`;
-          } else if (minutes > 0) {
-            return `${minutes}m ${remainingSeconds}s`;
-          } else {
-            return `${remainingSeconds.toFixed(2)}s`;
-          }
-        };
-
-        return (
-          <div>{customFormatter(row.original.execution_time ?? null)}</div>
-        );
-      },
-    }),
-
-    columnHelper.display({
-      id: "started",
-      header: "Started at",
-      cell: ({ row }) => {
-        const customFormatter: Formatter = (
-          value: number,
-          unit: Unit,
-          suffix: Suffix
-        ) => {
-          if (!row?.original?.started) {
-            return "";
-          }
-
-          const formattedString = formatDistanceToNowStrict(
-            new Date(row.original.started + "Z"),
-            { addSuffix: true }
-          );
-
-          return formattedString
-            .replace("about ", "")
-            .replace("minute", "min")
-            .replace("second", "sec")
-            .replace("hour", "hr");
-        };
-        return (
-          <TimeAgo
-            date={row.original.started + "Z"}
-            formatter={customFormatter}
-          />
-        );
-      },
-    }),
-    columnHelper.display({
-      id: "menu",
-      header: "",
-      cell: ({ row }) => <ExecutionRowMenu row={row} />,
-    }),
-  ] as DisplayColumnDef<WorkflowExecutionDetail>[];
-
-  //To DO pagiantion limit and offest can also be added to url searchparams
-  return (
-    <GenericTable<WorkflowExecutionDetail>
-      data={executions.items}
-      columns={columns}
-      rowCount={executions.count ?? 0} // Assuming pagination is not needed, you can adjust this if you have pagination
-      offset={executions.offset} // Customize as needed
-      limit={executions.limit} // Customize as needed
-      onPaginationChange={(newLimit: number, newOffset: number) =>
-        setPagination({ limit: newLimit, offset: newOffset })
-      }
-      onRowClick={(row: WorkflowExecutionDetail) => {
-        router.push(`/workflows/${row.workflow_id}/runs/${row.id}`);
-      }}
-    />
-  );
-}
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-executions-table.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-executions-table.tsx
new file mode 100644
index 0000000000..572493cfbc
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-executions-table.tsx
@@ -0,0 +1,250 @@
+import { Dispatch, SetStateAction } from "react";
+import {
+  createColumnHelper,
+  DisplayColumnDef,
+  Row,
+} from "@tanstack/react-table";
+import {
+  PaginatedWorkflowExecutionDto,
+  WorkflowExecutionDetail,
+} from "@/shared/api/workflow-executions";
+import { GenericTable } from "@/components/table/GenericTable";
+import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
+import TimeAgo, { Formatter, Suffix, Unit } from "react-timeago";
+import { formatDistanceToNowStrict } from "date-fns";
+import { Badge } from "@tremor/react";
+import { useRouter } from "next/navigation";
+import {
+  ArrowUpRightIcon,
+  ClipboardDocumentIcon,
+} from "@heroicons/react/24/outline";
+import { DropdownMenu, getIconForStatusString } from "@/shared/ui";
+import { Link } from "@/components/ui";
+import {
+  extractTriggerDetailsV2,
+  getTriggerIcon,
+} from "@/entities/workflows/lib/ui-utils";
+
+interface Pagination {
+  limit: number;
+  offset: number;
+}
+
+interface WorkflowExecutionsTableProps {
+  workflowName: string;
+  executions: PaginatedWorkflowExecutionDto;
+  setPagination: Dispatch<SetStateAction<Pagination>>;
+  currentRevision: number;
+}
+
+function WorkflowExecutionRowMenu({
+  row,
+}: {
+  row: Row<WorkflowExecutionDetail>;
+}) {
+  const router = useRouter();
+  return (
+    <DropdownMenu.Menu
+      icon={EllipsisHorizontalIcon}
+      label=""
+      onClick={(e) => e.stopPropagation()}
+    >
+      <DropdownMenu.Item
+        icon={ArrowUpRightIcon}
+        label="View Logs"
+        onClick={() => {
+          router.push(
+            `/workflows/${row.original.workflow_id}/runs/${row.original.id}`
+          );
+        }}
+      />
+      <DropdownMenu.Item
+        icon={ClipboardDocumentIcon}
+        label="Copy Execution ID"
+        onClick={async () => {
+          try {
+            await navigator.clipboard.writeText(row.original.id);
+          } catch (err) {
+            console.error("Failed to copy execution id:", err);
+          }
+        }}
+      />
+    </DropdownMenu.Menu>
+  );
+}
+
+export function WorkflowExecutionsTable({
+  workflowName,
+  executions,
+  setPagination,
+  currentRevision,
+}: WorkflowExecutionsTableProps) {
+  const columnHelper = createColumnHelper<WorkflowExecutionDetail>();
+
+  const columns = [
+    columnHelper.display({
+      id: "status",
+      header: "Status",
+      cell: ({ row }) => {
+        const status = row.original.status;
+        return <div>{getIconForStatusString(status)}</div>;
+      },
+    }),
+    columnHelper.display({
+      id: "workflow_revision",
+      header: "Workflow",
+      cell: ({ row }) => {
+        return (
+          <>
+            <Link
+              href={`/workflows/${row.original.workflow_id}/runs/${row.original.id}`}
+            >
+              {workflowName} · Rev. {row.original.workflow_revision}
+            </Link>
+            {row.original.workflow_revision === currentRevision ? (
+              <Badge color="green" size="xs" className="ml-1">
+                Current
+              </Badge>
+            ) : null}
+          </>
+        );
+      },
+    }),
+    columnHelper.display({
+      id: "triggered_by",
+      header: "Triggered by",
+      cell: ({ row }) => {
+        const triggered_by = row.original.triggered_by;
+        const { type, details } = extractTriggerDetailsV2(triggered_by);
+
+        let detailsContent: React.ReactNode = type as string;
+
+        if (type === "incident") {
+          detailsContent = (
+            <Link
+              href={`/incidents/${details.id}`}
+              target="_blank"
+              onClick={(e) => e.stopPropagation()}
+            >
+              {details.name}
+            </Link>
+          );
+        }
+        if (type === "alert") {
+          detailsContent = (
+            <Link
+              href={`/alerts/feed/?cel=${encodeURIComponent(
+                `id=="${details.id}"`
+              )}`}
+              onClick={(e) => e.stopPropagation()}
+            >
+              Alert &quot;{details.name}&quot;
+            </Link>
+          );
+        }
+        if (type === "manual") {
+          detailsContent = `Manually by ${details.user}`;
+        }
+        if (type === "interval") {
+          detailsContent = `Interval`;
+        }
+        return (
+          <>
+            <Badge
+              color="gray"
+              tooltip={Object.entries(details)
+                .map(([key, value]) => `${key}: ${value}`)
+                .join(", ")}
+              icon={getTriggerIcon(type)}
+            >
+              {detailsContent}
+            </Badge>
+          </>
+        );
+      },
+    }),
+    columnHelper.display({
+      id: "execution_time",
+      header: "Execution Duration",
+      cell: ({ row }) => {
+        const customFormatter = (seconds: number | null) => {
+          if (seconds === undefined || seconds === null) {
+            return "";
+          }
+
+          if (seconds === 0) {
+            return "0s";
+          }
+
+          const hours = Math.floor(seconds / 3600);
+          const minutes = Math.floor((seconds % 3600) / 60);
+          const remainingSeconds = seconds % 60;
+
+          if (hours > 0) {
+            return `${hours} hr ${minutes}m ${remainingSeconds}s`;
+          } else if (minutes > 0) {
+            return `${minutes}m ${remainingSeconds}s`;
+          } else {
+            return `${remainingSeconds.toFixed(2)}s`;
+          }
+        };
+
+        return (
+          <div>{customFormatter(row.original.execution_time ?? null)}</div>
+        );
+      },
+    }),
+
+    columnHelper.display({
+      id: "started",
+      header: "Started at",
+      cell: ({ row }) => {
+        const customFormatter: Formatter = (
+          value: number,
+          unit: Unit,
+          suffix: Suffix
+        ) => {
+          if (!row?.original?.started) {
+            return "";
+          }
+
+          const formattedString = formatDistanceToNowStrict(
+            new Date(row.original.started + "Z"),
+            { addSuffix: true }
+          );
+
+          return formattedString
+            .replace("about ", "")
+            .replace("minute", "min")
+            .replace("second", "sec")
+            .replace("hour", "hr");
+        };
+        return (
+          <TimeAgo
+            date={row.original.started + "Z"}
+            formatter={customFormatter}
+          />
+        );
+      },
+    }),
+    columnHelper.display({
+      id: "menu",
+      header: "",
+      cell: ({ row }) => <WorkflowExecutionRowMenu row={row} />,
+    }),
+  ] as DisplayColumnDef<WorkflowExecutionDetail>[];
+
+  //To DO pagiantion limit and offest can also be added to url searchparams
+  return (
+    <GenericTable<WorkflowExecutionDetail>
+      data={executions.items}
+      columns={columns}
+      rowCount={executions.count ?? 0} // Assuming pagination is not needed, you can adjust this if you have pagination
+      offset={executions.offset} // Customize as needed
+      limit={executions.limit} // Customize as needed
+      onPaginationChange={(newLimit: number, newOffset: number) =>
+        setPagination({ limit: newLimit, offset: newOffset })
+      }
+    />
+  );
+}
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx
index 4426196d74..35b9bf1396 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx
@@ -6,7 +6,7 @@ import { useState, useEffect } from "react";
 import { Workflow } from "@/shared/api/workflows";
 import WorkflowGraph from "../workflow-graph";
 import { TableFilters } from "./table-filters";
-import { ExecutionTable } from "./workflow-execution-table";
+import { WorkflowExecutionsTable } from "./workflow-executions-table";
 import { WorkflowOverviewSkeleton } from "./workflow-overview-skeleton";
 import { WorkflowProviders } from "./workflow-providers";
 import { WorkflowSteps } from "../workflows-templates";
@@ -37,6 +37,7 @@ export default function WorkflowOverview({
   });
   const searchParams = useSearchParams();
 
+  // TODO: This is a hack to reset the pagination when the search params change. WHY?
   useEffect(() => {
     setExecutionPagination({
       ...executionPagination,
@@ -44,7 +45,7 @@ export default function WorkflowOverview({
     });
   }, [searchParams]);
 
-  const { data, isLoading, error, isValidating } = useWorkflowExecutionsV2(
+  const { data, isLoading, error } = useWorkflowExecutionsV2(
     workflow_id,
     executionPagination.limit,
     executionPagination.offset
@@ -149,8 +150,10 @@ export default function WorkflowOverview({
           </Card>
           <h1 className="text-xl font-bold mt-4">Execution History</h1>
           <TableFilters workflowId={data.workflow.id} />
-          <ExecutionTable
+          <WorkflowExecutionsTable
+            workflowName={data.workflow.name}
             executions={data}
+            currentRevision={data.workflow.revision ?? 0}
             setPagination={setExecutionPagination}
           />
         </div>
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-secrets.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-secrets.tsx
index 0078cfc49f..9438801030 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-secrets.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-secrets.tsx
@@ -59,8 +59,9 @@ const WorkflowSecrets = ({ workflowId }: { workflowId: string }) => {
           </div>
           <Button
             onClick={() => toggleShowValue(row.original.name)}
-            className="p-1 hover:bg-gray-100 rounded"
+            className="p-1 rounded"
             icon={showValues[row.original.name] ? EyeOff : Eye}
+            color="orange"
             variant="secondary"
           />
         </div>
@@ -84,52 +85,58 @@ const WorkflowSecrets = ({ workflowId }: { workflowId: string }) => {
   ];
 
   return (
-    <Card className="p-4">
-      <h2 className="text-xl font-semibold">Workflow Secrets</h2>
-
+    <>
       {error && (
         <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
           {error}
         </div>
       )}
 
-      <div className="flex gap-4 my-4">
-        <Input
-          type="text"
-          placeholder="Secret name"
-          value={newSecret.name}
-          onChange={(e) =>
-            setNewSecret((prev) => ({ ...prev, name: e.target.value }))
-          }
-        />
-        <Input
-          placeholder="Secret value"
-          value={newSecret.value}
-          onChange={(e) =>
-            setNewSecret((prev) => ({ ...prev, value: e.target.value }))
+      <Card className="p-4">
+        <div className="flex gap-4 my-4">
+          <Input
+            type="text"
+            placeholder="Secret name"
+            value={newSecret.name}
+            onChange={(e) =>
+              setNewSecret((prev) => ({ ...prev, name: e.target.value }))
+            }
+          />
+          <Input
+            placeholder="Secret value"
+            value={newSecret.value}
+            onChange={(e) =>
+              setNewSecret((prev) => ({ ...prev, value: e.target.value }))
+            }
+          />
+          <Button
+            onClick={handleAddSecret}
+            variant="primary"
+            color="orange"
+            icon={PlusIcon}
+          >
+            Add Secret
+          </Button>
+        </div>
+
+        <GenericTable
+          asCard={false}
+          data={
+            secrets
+              ? Object.entries(secrets).map(([name, value]) => ({
+                  name,
+                  value,
+                }))
+              : []
           }
+          columns={columns}
+          rowCount={secrets ? Object.keys(secrets).length : 0}
+          offset={0}
+          limit={10}
+          dataFetchedAtOneGO={true}
         />
-        <Button onClick={handleAddSecret} variant="primary" icon={PlusIcon}>
-          Add Secret
-        </Button>
-      </div>
-
-      <GenericTable
-        data={
-          secrets
-            ? Object.entries(secrets).map(([name, value]) => ({ name, value }))
-            : []
-        }
-        columns={columns}
-        rowCount={secrets ? Object.keys(secrets).length : 0}
-        offset={0}
-        limit={10}
-        onPaginationChange={(newOffset, newLimit) => {
-          console.log("Pagination changed:", newOffset, newLimit);
-        }}
-        dataFetchedAtOneGO={true}
-      />
-    </Card>
+      </Card>
+    </>
   );
 };
 
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-sync-status.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-sync-status.tsx
index 2c467e16af..923578038b 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-sync-status.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-sync-status.tsx
@@ -17,7 +17,7 @@ export function WorkflowSyncStatus({
   lastDeployedAt,
   isChangesSaved,
 }: WorkflowSyncStatusProps) {
-  const { workflow } = useWorkflowDetail(workflowId);
+  const { workflow } = useWorkflowDetail(workflowId, null);
 
   const lastSavedAt = workflow?.last_updated + "Z" || lastDeployedAt;
   const revision = workflow?.revision;
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-versions.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-versions.tsx
new file mode 100644
index 0000000000..48853756e2
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-versions.tsx
@@ -0,0 +1,158 @@
+import {
+  useWorkflowDetail,
+  useWorkflowRevisions,
+} from "@/entities/workflows/model";
+import { Badge, Card, Subtitle, Switch, Text } from "@tremor/react";
+import { format } from "date-fns";
+import { KeepLoader, WorkflowYAMLEditor } from "@/shared/ui";
+import { useMemo, useState } from "react";
+import { getOrderedWorkflowYamlString } from "@/entities/workflows/lib/yaml-utils";
+import UserAvatar from "@/components/navbar/UserAvatar";
+import clsx from "clsx";
+
+export function WorkflowVersions({
+  workflowId,
+  currentRevision,
+}: {
+  workflowId: string;
+  currentRevision: number | null;
+}) {
+  const [selectedRevision, setSelectedRevision] = useState<number | null>(
+    currentRevision
+  );
+  const [showDiff, setShowDiff] = useState(true);
+  const { data, isLoading, error } = useWorkflowRevisions(workflowId);
+  const { workflow } = useWorkflowDetail(workflowId, selectedRevision);
+  const previousRevision = useMemo(() => {
+    if (!selectedRevision || !data?.versions.length) {
+      return null;
+    }
+    const index = data.versions.findIndex(
+      (v) => v.revision === selectedRevision
+    );
+    const previousIndex = index + 1; // +1 because they sorted descending
+    if (previousIndex >= data.versions.length) {
+      return null;
+    }
+    return data.versions[previousIndex]?.revision;
+  }, [selectedRevision, data]);
+  const { workflow: previousWorkflow } = useWorkflowDetail(
+    previousRevision !== null && showDiff ? workflowId : null,
+    previousRevision
+  );
+
+  const uniqueYears = useMemo(() => {
+    return [
+      ...new Set(
+        (data?.versions ?? []).map((revision) => {
+          return format(new Date(revision.last_updated), "yyyy");
+        })
+      ),
+    ];
+  }, [data?.versions]);
+
+  let formatString = "MMM d, yyyy HH:mm:ss";
+  if (uniqueYears?.length === 1) {
+    formatString = "MMM d, HH:mm:ss";
+  }
+
+  if (error) {
+    return (
+      <div className="p-4">
+        <Text color="red">Error loading workflow revisions</Text>
+      </div>
+    );
+  }
+
+  if (data?.versions.length === 0) {
+    return (
+      <div className="p-4">
+        <Text>No revisions found for this workflow</Text>
+      </div>
+    );
+  }
+
+  // showing loader if loading is not yet started to avoid flash of content
+  if (isLoading || !data) {
+    return (
+      <div className="flex justify-center items-center h-48">
+        <KeepLoader
+          includeMinHeight={false}
+          loadingText="Loading workflow revisions"
+        />
+      </div>
+    );
+  }
+
+  const editorProps = previousWorkflow
+    ? {
+        original: getOrderedWorkflowYamlString(previousWorkflow.workflow_raw),
+        modified: getOrderedWorkflowYamlString(workflow?.workflow_raw ?? ""),
+      }
+    : {
+        value: getOrderedWorkflowYamlString(workflow?.workflow_raw ?? ""),
+      };
+
+  return (
+    <Card className="h-[calc(100vh-12rem)] flex p-0 overflow-hidden relative">
+      <div className="flex-1 p-[2px] h-full min-w-0 border-r border-gray-200">
+        <WorkflowYAMLEditor
+          filename={
+            workflow?.name.replaceAll(" ", "_") +
+            "_v" +
+            workflow?.revision +
+            ".yaml"
+          }
+          readOnly={true}
+          {...editorProps}
+        />
+      </div>
+      <div className="flex flex-col basis-1/4 min-w-0 justify-between">
+        <div className="flex flex-col overflow-y-auto">
+          {data.versions.map((revision) => {
+            let userName = revision.updated_by;
+            if (!userName && revision.revision === 1) {
+              userName = workflow?.created_by ?? "";
+            }
+            return (
+              <button
+                key={revision.revision}
+                className={clsx(
+                  "flex flex-col gap-1 border-b border-gray-200 p-2 text-left text-gray-800 text-sm",
+                  selectedRevision === revision.revision
+                    ? "bg-slate-200/70"
+                    : "hover:bg-slate-50"
+                )}
+                onClick={() => setSelectedRevision(revision.revision)}
+              >
+                <span className="font-bold flex items-center gap-1 leading-none">
+                  Revision {revision.revision}
+                  {currentRevision === revision.revision ? (
+                    <Badge color="green" size="xs" className="text-xs">
+                      Current
+                    </Badge>
+                  ) : null}
+                </span>
+                <span>
+                  {format(new Date(revision.last_updated), formatString)}
+                </span>
+                <span className="flex items-center gap-1">
+                  <UserAvatar size="xs" image={null} name={userName ?? ""} />{" "}
+                  <Subtitle className="truncate">{userName ?? ""}</Subtitle>
+                </span>
+              </button>
+            );
+          })}
+        </div>
+        <div className="flex items-center gap-2 min-h-0 p-2 text-sm">
+          <Switch
+            id="show-diff"
+            checked={showDiff}
+            onChange={() => setShowDiff(!showDiff)}
+          />
+          <label htmlFor="show-diff">Show diff from previous revision</label>
+        </div>
+      </div>
+    </Card>
+  );
+}
diff --git a/keep-ui/components/navbar/UserAvatar.tsx b/keep-ui/components/navbar/UserAvatar.tsx
index 5e6f8b54f2..84afeb5f86 100644
--- a/keep-ui/components/navbar/UserAvatar.tsx
+++ b/keep-ui/components/navbar/UserAvatar.tsx
@@ -13,6 +13,13 @@ export const getInitials = (name: string) =>
     .join("")
     .toUpperCase();
 
+const getBackgroundColor = (name: string) => {
+  const hash = name.split("").reduce((acc, char) => {
+    return acc + char.charCodeAt(0);
+  }, 0);
+  return `#${hash.toString(16).padStart(6, "0")}`;
+};
+
 export default function UserAvatar({ image, name, size = "sm", email }: Props) {
   const sizeClass = (function (size: "sm" | "xs") {
     if (size === "sm") return "w-7 h-7";
@@ -34,12 +41,18 @@ export default function UserAvatar({ image, name, size = "sm", email }: Props) {
   ) : (
     <span
       className={clsx(
-        "relative inline-flex items-center justify-center overflow-hidden bg-orange-400 rounded-full dark:bg-gray-600",
+        "relative inline-flex items-center justify-center overflow-hidden rounded-full dark:bg-gray-600",
         sizeClass
       )}
+      style={{ backgroundColor: getBackgroundColor(name) }}
       title={email ?? name}
     >
-      <span className="font-medium text-white text-xs">
+      <span
+        className={clsx(
+          "font-medium text-white text-xs",
+          size === "xs" ? "text-[0.6rem]" : "text-xs"
+        )}
+      >
         {getInitials(name)}
       </span>
     </span>
diff --git a/keep-ui/components/table/ExecutionsTable.tsx b/keep-ui/components/table/ExecutionsTable.tsx
index 8de55b2b10..306d7c3cea 100644
--- a/keep-ui/components/table/ExecutionsTable.tsx
+++ b/keep-ui/components/table/ExecutionsTable.tsx
@@ -6,9 +6,9 @@ import {
 } from "@/shared/api/enrichment-events";
 import { Dispatch, SetStateAction } from "react";
 import { useRouter } from "next/navigation";
-import { getIcon } from "../../app/(keep)/workflows/[workflow_id]/workflow-execution-table";
 import TimeAgo from "react-timeago";
 import { formatDistanceToNowStrict } from "date-fns";
+import { getIconForStatusString } from "@/shared/ui";
 
 interface Pagination {
   limit: number;
@@ -30,7 +30,7 @@ export function ExecutionsTable({ executions, setPagination }: Props) {
       header: "Status",
       cell: ({ row }) => {
         const status = row.original.status;
-        return <div>{getIcon(status)}</div>;
+        return <div>{getIconForStatusString(status)}</div>;
       },
     }),
     columnHelper.display({
diff --git a/keep-ui/components/table/GenericTable.tsx b/keep-ui/components/table/GenericTable.tsx
index a02dbdb910..68953564b1 100644
--- a/keep-ui/components/table/GenericTable.tsx
+++ b/keep-ui/components/table/GenericTable.tsx
@@ -16,6 +16,7 @@ import {
 } from "@tanstack/react-table";
 import React, { useEffect, useState } from "react";
 import Pagination from "./Pagination";
+import clsx from "clsx";
 
 interface GenericTableProps<T> {
   data: T[];
@@ -23,9 +24,10 @@ interface GenericTableProps<T> {
   rowCount: number;
   offset: number;
   limit: number;
-  onPaginationChange: (limit: number, offset: number) => void;
+  onPaginationChange?: (limit: number, offset: number) => void;
   onRowClick?: (row: T) => void;
   dataFetchedAtOneGO?: boolean;
+  asCard?: boolean;
 }
 
 export function GenericTable<T>({
@@ -37,6 +39,7 @@ export function GenericTable<T>({
   onPaginationChange,
   onRowClick,
   dataFetchedAtOneGO,
+  asCard = true,
 }: GenericTableProps<T>) {
   const [expanded, setExpanded] = useState<ExpandedState>({});
   const [pagination, setPagination] = useState({
@@ -54,7 +57,7 @@ export function GenericTable<T>({
   useEffect(() => {
     const currentOffset = pagination.pageSize * pagination.pageIndex;
     if (offset !== currentOffset || limit !== pagination.pageSize) {
-      onPaginationChange(pagination.pageSize, currentOffset);
+      onPaginationChange?.(pagination.pageSize, currentOffset);
     }
   }, [pagination]);
 
@@ -82,9 +85,11 @@ export function GenericTable<T>({
     onExpandedChange: setExpanded,
   });
 
+  const Container = asCard ? Card : "div";
+
   return (
     <div className="flex flex-col gap-4 w-full h-full max-h-full">
-      <Card className="p-0">
+      <Container className="p-0">
         <TremorTable className="w-full">
           <TableHead>
             {table.getHeaderGroups().map((headerGroup) => (
@@ -109,7 +114,9 @@ export function GenericTable<T>({
           <TableBody className="bg-gray-20">
             {table.getRowModel().rows.map((row) => (
               <TableRow
-                className=" hover:bg-slate-100 cursor-pointer"
+                className={clsx(
+                  onRowClick && "hover:bg-slate-100 cursor-pointer"
+                )}
                 key={row.id}
                 onClick={() => onRowClick?.(row.original)}
               >
@@ -122,7 +129,7 @@ export function GenericTable<T>({
             ))}
           </TableBody>
         </TremorTable>
-      </Card>
+      </Container>
       {pagination && <Pagination table={table} isRefreshAllowed={false} />}
     </div>
   );
diff --git a/keep-ui/entities/workflows/lib/ui-utils.tsx b/keep-ui/entities/workflows/lib/ui-utils.tsx
new file mode 100644
index 0000000000..0f3078aedb
--- /dev/null
+++ b/keep-ui/entities/workflows/lib/ui-utils.tsx
@@ -0,0 +1,179 @@
+import {
+  ClockIcon,
+  CursorArrowRaysIcon,
+  QuestionMarkCircleIcon,
+} from "@heroicons/react/24/outline";
+import Image from "next/image";
+
+const KeepIncidentIcon = () => (
+  <Image
+    src="/keep.png"
+    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
+    width={16}
+    height={16}
+    alt="Keep Incident"
+  />
+);
+const KeepAlertIcon = () => (
+  <Image
+    src="/keep.png"
+    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
+    width={16}
+    height={16}
+    alt="Keep Alert"
+  />
+);
+
+export function getTriggerIcon(triggered_by: string) {
+  switch (triggered_by) {
+    case "manual":
+      return CursorArrowRaysIcon;
+    case "interval":
+      return ClockIcon;
+    case "alert":
+      return KeepAlertIcon;
+    case "incident":
+      return KeepIncidentIcon;
+    default:
+      return QuestionMarkCircleIcon;
+  }
+}
+
+export function getTriggerIconV2(type: string, details: string) {
+  switch (type) {
+    case "manual":
+      return CursorArrowRaysIcon;
+    case "interval":
+      return ClockIcon;
+    case "alert":
+      return KeepAlertIcon;
+    case "incident":
+      return KeepIncidentIcon;
+    default:
+      return QuestionMarkCircleIcon;
+  }
+}
+
+export function extractTriggerValue(triggered_by: string | undefined): string {
+  if (!triggered_by) return "others";
+
+  if (triggered_by.startsWith("scheduler")) {
+    return "interval";
+  } else if (triggered_by.startsWith("type:alert")) {
+    return "alert";
+  } else if (triggered_by.startsWith("manually")) {
+    return triggered_by;
+  } else if (triggered_by.startsWith("type:incident:")) {
+    const incidentType = triggered_by
+      .substring("type:incident:".length)
+      .split(" ")[0];
+    return `incident ${incidentType}`;
+  } else {
+    return "others";
+  }
+}
+
+export function extractTriggerType(
+  triggered_by: string | undefined
+): "interval" | "alert" | "manual" | "incident" | "unknown" {
+  if (!triggered_by) {
+    return "unknown";
+  }
+
+  if (triggered_by.startsWith("scheduler")) {
+    return "interval";
+  } else if (triggered_by.startsWith("type:alert")) {
+    return "alert";
+  } else if (triggered_by.startsWith("manually")) {
+    return "manual";
+  } else if (triggered_by.startsWith("type:incident:")) {
+    return "incident";
+  } else {
+    return "unknown";
+  }
+}
+
+export function extractTriggerDetails(
+  triggered_by: string | undefined
+): string[] {
+  if (!triggered_by) {
+    return [];
+  }
+
+  let details: string;
+  if (triggered_by.startsWith("scheduler")) {
+    details = triggered_by.substring("scheduler".length).trim();
+  } else if (triggered_by.startsWith("type:alert")) {
+    details = triggered_by.substring("type:alert".length).trim();
+  } else if (triggered_by.startsWith("manual")) {
+    details = triggered_by.substring("manual".length).trim();
+  } else if (triggered_by.startsWith("type:incident:")) {
+    // Handle 'type:incident:{some operator}' by removing the operator
+    details = triggered_by.substring("type:incident:".length).trim();
+    const firstSpaceIndex = details.indexOf(" ");
+    if (firstSpaceIndex > -1) {
+      details = details.substring(firstSpaceIndex).trim();
+    } else {
+      details = "";
+    }
+  } else {
+    details = triggered_by;
+  }
+
+  // Split the string into key-value pairs, where values may contain spaces
+  const regex = /\b(\w+:[^:]+?)(?=\s\w+:|$)/g;
+  const matches = details.match(regex);
+
+  return matches ?? [];
+}
+
+type TriggerDetails = {
+  type: "manual" | "interval" | "alert" | "incident" | "unknown";
+  details: Record<string, string>;
+};
+
+export function extractTriggerDetailsV2(
+  triggered_by: string | undefined
+): TriggerDetails {
+  if (!triggered_by) {
+    return { type: "unknown", details: {} };
+  }
+
+  let type: TriggerDetails["type"] = extractTriggerType(triggered_by);
+  let details: string;
+  if (triggered_by.startsWith("scheduler")) {
+    // details = triggered_by.substring("scheduler".length).trim();
+    details = "scheduler";
+  } else if (triggered_by.startsWith("type:alert")) {
+    details = triggered_by.substring("type:alert".length).trim();
+  } else if (triggered_by.startsWith("manually by")) {
+    details = "user:" + triggered_by.substring("manually by".length).trim();
+  } else if (triggered_by.startsWith("type:incident:")) {
+    // Handle 'type:incident:{some operator}' by removing the operator
+    details = triggered_by.substring("type:incident:".length).trim();
+    const firstSpaceIndex = details.indexOf(" ");
+    if (firstSpaceIndex > -1) {
+      details = details.substring(firstSpaceIndex).trim();
+    } else {
+      details = "";
+    }
+  } else {
+    details = triggered_by;
+  }
+
+  // Split the string into key-value pairs, where values may contain spaces
+  const regex = /\b(\w+:[^:]+?)(?=\s\w+:|$)/g;
+  const matches = details.match(regex);
+
+  return {
+    type,
+    details: matches
+      ? Object.fromEntries(
+          matches.map((match) => {
+            const [key, value] = match.split(":");
+            return [key, value];
+          })
+        )
+      : {},
+  };
+}
diff --git a/keep-ui/entities/workflows/model/__tests__/useWorkflowRevalidation.test.tsx b/keep-ui/entities/workflows/model/__tests__/useWorkflowRevalidation.test.tsx
index 00292426fd..3e95343dc3 100644
--- a/keep-ui/entities/workflows/model/__tests__/useWorkflowRevalidation.test.tsx
+++ b/keep-ui/entities/workflows/model/__tests__/useWorkflowRevalidation.test.tsx
@@ -41,7 +41,7 @@ describe("useWorkflowRevalidation", () => {
     result.current.revalidateDetail(mockWorkflowId);
 
     expect(mockMutate).toHaveBeenCalledWith(
-      workflowKeys.detail(mockWorkflowId)
+      workflowKeys.detail(mockWorkflowId, null)
     );
   });
 
@@ -50,17 +50,21 @@ describe("useWorkflowRevalidation", () => {
 
     result.current.revalidateWorkflow(mockWorkflowId);
 
-    expect(mockMutate).toHaveBeenCalledTimes(2);
+    // one for the lists, one for the revisions, one for the detail
+    expect(mockMutate).toHaveBeenCalledTimes(3);
+
+    const [firstCall, secondCall, thirdCall] = mockMutate.mock.calls;
 
     // Verify list matcher function
-    const listMatcherFunction = mockMutate.mock.calls[0][0];
+    const listMatcherFunction = firstCall[0];
     expect(typeof listMatcherFunction).toBe("function");
     expect(listMatcherFunction("workflows::list::")).toBe(true);
     expect(listMatcherFunction("workflows::detail::")).toBe(false);
 
     // Verify detail key
-    expect(mockMutate.mock.calls[1][0]).toBe(
-      workflowKeys.detail(mockWorkflowId)
-    );
+    expect(thirdCall[0]).toBe(workflowKeys.detail(mockWorkflowId, null));
+
+    // Verify revisions key
+    expect(secondCall[0]).toBe(workflowKeys.revisions(mockWorkflowId));
   });
 });
diff --git a/keep-ui/entities/workflows/model/index.ts b/keep-ui/entities/workflows/model/index.ts
new file mode 100644
index 0000000000..cd81e5a82f
--- /dev/null
+++ b/keep-ui/entities/workflows/model/index.ts
@@ -0,0 +1,7 @@
+export { useWorkflowRevisions } from "./useWorkflowRevisions";
+export { useWorkflowDetail } from "./useWorkflowDetail";
+export { useWorkflows } from "./useWorkflows";
+export { useWorkflowsV2 } from "./useWorkflowsV2";
+export { useWorkflowActions } from "./useWorkflowActions";
+export { useWorkflowRevalidation } from "./useWorkflowRevalidation";
+export { workflowKeys } from "./workflowKeys";
diff --git a/keep-ui/entities/workflows/model/useWorkflowDetail.ts b/keep-ui/entities/workflows/model/useWorkflowDetail.ts
index 245a8d87b2..cefd703d12 100644
--- a/keep-ui/entities/workflows/model/useWorkflowDetail.ts
+++ b/keep-ui/entities/workflows/model/useWorkflowDetail.ts
@@ -5,22 +5,25 @@ import { workflowKeys } from "./workflowKeys";
 
 export function useWorkflowDetail(
   workflowId: string | null,
+  workflowRevision: number | null,
   options?: SWRConfiguration<Workflow>
 ) {
   const api = useApi();
 
   const cacheKey =
-    api.isReady() && workflowId ? workflowKeys.detail(workflowId) : null;
+    api.isReady() && workflowId
+      ? workflowKeys.detail(workflowId, workflowRevision)
+      : null;
+
+  const requestUrl = workflowRevision
+    ? `/workflows/${workflowId}/versions/${workflowRevision}`
+    : `/workflows/${workflowId}`;
 
   const {
     data: workflow,
     error,
     isLoading,
-  } = useSWR<Workflow>(
-    cacheKey,
-    () => api.get(`/workflows/${workflowId}`),
-    options
-  );
+  } = useSWR<Workflow>(cacheKey, () => api.get(requestUrl), options);
 
   return {
     workflow,
diff --git a/keep-ui/entities/workflows/model/useWorkflowRevalidation.ts b/keep-ui/entities/workflows/model/useWorkflowRevalidation.ts
index c0a804a2e4..04254cde26 100644
--- a/keep-ui/entities/workflows/model/useWorkflowRevalidation.ts
+++ b/keep-ui/entities/workflows/model/useWorkflowRevalidation.ts
@@ -18,24 +18,33 @@ export function useWorkflowRevalidation() {
   /**
    * Revalidates a specific workflow by ID
    */
-  const revalidateDetail = useCallback((workflowId: string) => {
-    return mutate(workflowKeys.detail(workflowId));
+  const revalidateDetail = useCallback(
+    (workflowId: string, workflowRevision: number | null = null) => {
+      return mutate(workflowKeys.detail(workflowId, workflowRevision));
+    },
+    []
+  );
+
+  const revalidateWorkflowRevisions = useCallback((workflowId: string) => {
+    return mutate(workflowKeys.revisions(workflowId));
   }, []);
 
   /**
    * Revalidates both the lists and a specific workflow detail
    */
   const revalidateWorkflow = useCallback(
-    (workflowId: string) => {
+    (workflowId: string, workflowRevision: number | null = null) => {
       revalidateLists();
-      revalidateDetail(workflowId);
+      revalidateWorkflowRevisions(workflowId);
+      revalidateDetail(workflowId, workflowRevision);
     },
-    [revalidateLists, revalidateDetail]
+    [revalidateLists, revalidateDetail, revalidateWorkflowRevisions]
   );
 
   return {
     revalidateLists,
     revalidateDetail,
     revalidateWorkflow,
+    revalidateWorkflowRevisions,
   };
 }
diff --git a/keep-ui/entities/workflows/model/useWorkflowRevisions.ts b/keep-ui/entities/workflows/model/useWorkflowRevisions.ts
new file mode 100644
index 0000000000..40bb136129
--- /dev/null
+++ b/keep-ui/entities/workflows/model/useWorkflowRevisions.ts
@@ -0,0 +1,20 @@
+import useSWR, { SWRConfiguration } from "swr";
+import { useApi } from "@/shared/lib/hooks/useApi";
+import { workflowKeys } from "./workflowKeys";
+import { WorkflowRevisionList } from "@/shared/api/workflows";
+
+export function useWorkflowRevisions(
+  workflowId: string | null,
+  options?: SWRConfiguration<WorkflowRevisionList>
+) {
+  const api = useApi();
+
+  const cacheKey =
+    api.isReady() && workflowId ? workflowKeys.revisions(workflowId) : null;
+
+  return useSWR<WorkflowRevisionList>(
+    cacheKey,
+    () => api.get(`/workflows/${workflowId}/versions`),
+    options
+  );
+}
diff --git a/keep-ui/entities/workflows/model/workflowKeys.ts b/keep-ui/entities/workflows/model/workflowKeys.ts
index 37f55f9a72..e4a8e9270a 100644
--- a/keep-ui/entities/workflows/model/workflowKeys.ts
+++ b/keep-ui/entities/workflows/model/workflowKeys.ts
@@ -14,7 +14,10 @@ export const workflowKeys = {
     ]
       .filter((p) => p !== undefined && p !== null)
       .join("::"),
-  detail: (id: string) => [workflowKeys.all, "detail", id].join("::"),
+  detail: (id: string, revision: number | null) =>
+    [workflowKeys.all, "detail", id, revision].join("::"),
+  revisions: (workflowId: string) =>
+    [workflowKeys.all, "revisions", workflowId].join("::"),
   getListMatcher: () => (key: any) =>
     typeof key === "string" &&
     key.startsWith([workflowKeys.all, "list"].join("::")),
diff --git a/keep-ui/features/incidents/incident-list/ui/incident-table-component.tsx b/keep-ui/features/incidents/incident-list/ui/incident-table-component.tsx
index 3b76f5c1ed..c3e85d2ef1 100644
--- a/keep-ui/features/incidents/incident-list/ui/incident-table-component.tsx
+++ b/keep-ui/features/incidents/incident-list/ui/incident-table-component.tsx
@@ -7,11 +7,11 @@ import {
   TableHeaderCell,
   TableRow,
 } from "@tremor/react";
+import clsx from "clsx";
 import { flexRender, Header, Table as ReactTable } from "@tanstack/react-table";
 import React, { ReactNode } from "react";
 import { IncidentDto } from "@/entities/incidents/model";
 import { FaArrowDown, FaArrowRight, FaArrowUp } from "react-icons/fa";
-import clsx from "clsx";
 import { getCommonPinningStylesAndClassNames } from "@/shared/ui";
 
 interface Props {
diff --git a/keep-ui/features/workflow-execution-results/ui/WorkflowExecutionResults.tsx b/keep-ui/features/workflow-execution-results/ui/WorkflowExecutionResults.tsx
index c53b04491b..e22370b155 100644
--- a/keep-ui/features/workflow-execution-results/ui/WorkflowExecutionResults.tsx
+++ b/keep-ui/features/workflow-execution-results/ui/WorkflowExecutionResults.tsx
@@ -1,7 +1,7 @@
 "use client";
 
 import React, { useEffect, useState, useMemo, useRef } from "react";
-import { Card, Callout, Button } from "@tremor/react";
+import { Card, Callout, Button, Badge } from "@tremor/react";
 import Loading from "@/app/(keep)/loading";
 import {
   ArrowPathIcon,
@@ -21,8 +21,9 @@ import { EmptyStateCard, MonacoEditor, ResizableColumns } from "@/shared/ui";
 import { WorkflowYAMLEditorWithLogs } from "@/shared/ui/WorkflowYAMLEditorWithLogs";
 import { useWorkflowExecutionDetail } from "@/entities/workflow-executions/model/useWorkflowExecutionDetail";
 import { useWorkflowDetail } from "@/entities/workflows/model/useWorkflowDetail";
-import clsx from "clsx";
+import { Workflow } from "@/shared/api/workflows";
 import { useWorkflowExecutionsRevalidation } from "@/entities/workflow-executions/model/useWorkflowExecutionsRevalidation";
+import clsx from "clsx";
 
 const WAIT_AFTER_STATUS_CHANGED = 2000;
 
@@ -76,8 +77,15 @@ export function WorkflowExecutionResults({
       : undefined,
   });
 
+  const workflowRevision = isWorkflowExecution(executionData)
+    ? executionData.workflow_revision
+    : undefined;
+
+  const { workflow: latestWorkflowData } = useWorkflowDetail(workflowId, null);
+
   const { workflow: workflowData, error: workflowError } = useWorkflowDetail(
-    !workflowYaml ? workflowId : null
+    !workflowYaml ? workflowId : null,
+    workflowRevision ?? null
   );
 
   const finalYaml = workflowYaml ?? workflowData?.workflow_raw;
@@ -130,35 +138,17 @@ export function WorkflowExecutionResults({
     };
   }, []);
 
-  if (!executionData || !finalYaml) {
+  if (!finalYaml && !workflowError && !executionError) {
     return <Loading />;
   }
 
-  if (executionError) {
-    return (
-      <Callout
-        className="mt-4"
-        title="Error"
-        icon={ExclamationCircleIcon}
-        color="rose"
-      >
-        Failed to load workflow execution
-      </Callout>
-    );
-  }
-
-  if (workflowError) {
-    return (
-      <Callout title="Error" icon={ExclamationCircleIcon} color="rose">
-        Failed to load workflow definition
-      </Callout>
-    );
-  }
-
   return (
     <WorkflowExecutionResultsInternal
-      workflowId={workflowId}
-      executionData={executionData}
+      workflow={workflowData ?? null}
+      workflowError={workflowError ?? null}
+      executionError={executionError ?? null}
+      isLatestRevision={workflowData?.revision === latestWorkflowData?.revision}
+      executionData={executionData ?? null}
       workflowRaw={finalYaml}
       checks={checks}
       isLoading={refreshInterval > 0}
@@ -170,19 +160,25 @@ export function WorkflowExecutionResults({
 const editorHeightClassName = "h-[calc(100vh-220px)]";
 
 export function WorkflowExecutionResultsInternal({
-  workflowId,
+  workflow,
+  workflowError,
   executionData,
+  executionError,
   workflowRaw,
+  isLatestRevision,
   checks,
   isLoading,
   isRevalidating,
 }: {
-  executionData: WorkflowExecutionDetail | WorkflowExecutionFailure;
+  executionData: WorkflowExecutionDetail | WorkflowExecutionFailure | null;
+  workflow: Workflow | null;
+  workflowError: Error | null;
+  executionError: Error | null;
   isRevalidating: boolean;
-  workflowId: string;
   workflowRaw: string | undefined;
   checks: number;
   isLoading: boolean;
+  isLatestRevision: boolean;
 }) {
   const [hoveredStep, setHoveredStep] = useState<string | null>(null);
   const [selectedStep, setSelectedStep] = useState<string | null>(null);
@@ -208,8 +204,8 @@ export function WorkflowExecutionResultsInternal({
     : null;
 
   const refreshExecutionData = () => {
-    if (executionId) {
-      revalidateForWorkflowExecution(workflowId, executionId);
+    if (executionId && workflow) {
+      revalidateForWorkflowExecution(workflow.id, executionId);
     }
   };
 
@@ -237,27 +233,59 @@ export function WorkflowExecutionResultsInternal({
 
   const tabs = [
     {
-      name: "Workflow Definition",
+      id: "workflow-definition",
+      name: (
+        <span className="flex items-center gap-2">
+          Workflow Definition
+          {isLatestRevision ? (
+            <Badge color="green" size="xs">
+              Current
+            </Badge>
+          ) : (
+            <Badge color="gray" size="xs">
+              Rev.{" "}
+              {isWorkflowExecution(executionData)
+                ? executionData.workflow_revision
+                : "unknown"}
+            </Badge>
+          )}
+        </span>
+      ),
       content: (
         <div className={editorHeightClassName}>
-          <WorkflowYAMLEditorWithLogs
-            workflowYamlString={workflowRaw ?? ""}
-            workflowId={workflowId}
-            executionLogs={logs}
-            executionStatus={status}
-            hoveredStep={hoveredStep}
-            setHoveredStep={setHoveredStep}
-            selectedStep={selectedStep}
-            setSelectedStep={setSelectedStep}
-            readOnly={true}
-            filename={workflowId}
-          />
+          {workflow && !workflowError ? (
+            <WorkflowYAMLEditorWithLogs
+              value={workflowRaw ?? ""}
+              workflowId={workflow.id}
+              executionLogs={logs}
+              executionStatus={status}
+              hoveredStep={hoveredStep}
+              setHoveredStep={setHoveredStep}
+              selectedStep={selectedStep}
+              setSelectedStep={setSelectedStep}
+              readOnly={true}
+              filename={workflow.id}
+            />
+          ) : (
+            <Callout
+              title="Error"
+              icon={ExclamationCircleIcon}
+              color="rose"
+              className="mx-4"
+            >
+              Failed to load workflow definition for revision{" "}
+              {isWorkflowExecution(executionData)
+                ? executionData.workflow_revision
+                : "unknown"}
+            </Callout>
+          )}
         </div>
       ),
     },
     ...(hasEvent
       ? [
           {
+            id: "event-trigger",
             name: "Event Trigger",
             content:
               typeof eventData === "object" ? (
@@ -298,18 +326,32 @@ export function WorkflowExecutionResultsInternal({
     />
   );
 
+  console.log("executionData", executionData);
+
   return (
     <div className="flex flex-col gap-4">
-      {executionData.error && (
-        <WorkflowExecutionError
-          error={executionData.error}
-          workflowId={workflowId}
-          eventId={eventId}
-          eventType={eventType}
-        />
-      )}
       <ResizableColumns initialLeftWidth={50}>
         <div className="pr-2">
+          {executionError && (
+            <Callout
+              className="mb-4"
+              title="Error"
+              icon={ExclamationCircleIcon}
+              color="rose"
+            >
+              Failed to load workflow execution
+            </Callout>
+          )}
+          {isWorkflowFailure(executionData) && (
+            <div className="mb-4">
+              <WorkflowExecutionError
+                error={executionData.error}
+                workflowId={workflow?.id}
+                eventId={eventId}
+                eventType={eventType}
+              />
+            </div>
+          )}
           {logs ? (
             <div className="flex flex-col gap-4 items-center">
               <Card className="p-0 overflow-hidden">
@@ -353,12 +395,12 @@ export function WorkflowExecutionResultsInternal({
             <TabGroup>
               <TabList className="p-2">
                 {tabs.map((tab) => (
-                  <Tab key={tab.name}>{tab.name}</Tab>
+                  <Tab key={tab.id}>{tab.name}</Tab>
                 ))}
               </TabList>
               <TabPanels>
                 {tabs.map((tab) => (
-                  <TabPanel key={tab.name}>{tab.content}</TabPanel>
+                  <TabPanel key={tab.id}>{tab.content}</TabPanel>
                 ))}
               </TabPanels>
             </TabGroup>
diff --git a/keep-ui/shared/api/workflow-executions.ts b/keep-ui/shared/api/workflow-executions.ts
index 5759c6cf15..05739a4daf 100644
--- a/keep-ui/shared/api/workflow-executions.ts
+++ b/keep-ui/shared/api/workflow-executions.ts
@@ -18,6 +18,7 @@ export interface WorkflowExecutionDetail {
   status: string;
   triggered_by: string;
   workflow_id: string;
+  workflow_revision: number;
   workflow_name?: string;
   tenant_id: string;
 }
@@ -38,9 +39,14 @@ export type WorkflowExecutionFailure = Pick<WorkflowExecutionDetail, "error">;
 export function isWorkflowExecution(
   data: any
 ): data is WorkflowExecutionDetail {
-  return typeof data === "object" && "id" in data;
+  return data !== null && typeof data === "object" && "id" in data;
 }
 
 export function isWorkflowFailure(data: any): data is WorkflowExecutionFailure {
-  return typeof data === "object" && "error" in data;
+  return (
+    data !== null &&
+    typeof data === "object" &&
+    "error" in data &&
+    data.error !== null
+  );
 }
diff --git a/keep-ui/shared/api/workflows.ts b/keep-ui/shared/api/workflows.ts
index 612685e13f..d9caa75669 100644
--- a/keep-ui/shared/api/workflows.ts
+++ b/keep-ui/shared/api/workflows.ts
@@ -128,6 +128,16 @@ export type PaginatedWorkflowsResults = {
   offset: number;
 };
 
+export type WorkflowRevision = {
+  revision: number;
+  updated_by: string;
+  last_updated: string;
+};
+
+export type WorkflowRevisionList = {
+  versions: WorkflowRevision[];
+};
+
 export async function getWorkflow(api: ApiClient, id: string) {
   return await api.get<Workflow>(`/workflows/${id}`);
 }
diff --git a/keep-ui/shared/ui/MonacoYAMLEditor/MonacoYAMLEditor.types.ts b/keep-ui/shared/ui/MonacoYAMLEditor/MonacoYAMLEditor.types.ts
new file mode 100644
index 0000000000..8912ef3df8
--- /dev/null
+++ b/keep-ui/shared/ui/MonacoYAMLEditor/MonacoYAMLEditor.types.ts
@@ -0,0 +1,11 @@
+import { EditorProps } from "@monaco-editor/react";
+
+export type MonacoYamlEditorProps = {
+  schemas: {
+    fileMatch: string[];
+    schema: object;
+    uri: string;
+  }[];
+  original?: string;
+  modified?: string;
+} & EditorProps;
diff --git a/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.tsx b/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.tsx
index f55a3ef437..39a37d1ef2 100644
--- a/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.tsx
+++ b/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.tsx
@@ -2,10 +2,11 @@
 
 import { useEffect, useState } from "react";
 import { configureMonacoYaml, MonacoYaml } from "monaco-yaml";
-import { Editor, loader, type EditorProps } from "@monaco-editor/react";
+import { DiffEditor, Editor, loader } from "@monaco-editor/react";
 import { useConfig } from "@/utils/hooks/useConfig";
 import { KeepLoader, ErrorComponent } from "@/shared/ui";
 import * as monaco from "monaco-editor";
+import { MonacoYamlEditorProps } from "./MonacoYAMLEditor.types";
 
 // Loading these workers from NPM only works with webpack. For turbopack, we use 'editor.client.turbopack.tsx'
 self.MonacoEnvironment = {
@@ -39,19 +40,16 @@ const Loader = <KeepLoader loadingText="Loading Code Editor ..." />;
 // In the docs, it is stated that there should only be one monaco yaml instance configured at a time
 let monacoYamlInstance: MonacoYaml | undefined;
 
-type MonacoYamlEditorProps = {
-  schemas: {
-    fileMatch: string[];
-    schema: object;
-    uri: string;
-  }[];
-} & EditorProps;
-
 /**
  * This is a custom editor component that uses 'monaco-yaml' to provide YAML language support.
  * It is used to edit YAML files.
  */
-export function MonacoYAMLEditor({ schemas, ...props }: MonacoYamlEditorProps) {
+export function MonacoYAMLEditor({
+  schemas,
+  original,
+  modified,
+  ...props
+}: MonacoYamlEditorProps) {
   const [isMonacoInitialized, setIsMonacoInitialized] = useState(false);
 
   useEffect(() => {
@@ -108,5 +106,17 @@ export function MonacoYAMLEditor({ schemas, ...props }: MonacoYamlEditorProps) {
     );
   }
 
-  return <Editor defaultLanguage="yaml" loading={Loader} {...props} />;
+  if (original && modified) {
+    return (
+      // @ts-expect-error - DiffEditorProps is not typed correctly yet
+      <DiffEditor
+        original={original}
+        modified={modified}
+        language="yaml"
+        loading={Loader}
+        {...props}
+      />
+    );
+  }
+  return <Editor language="yaml" loading={Loader} {...props} />;
 }
diff --git a/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.turbopack.tsx b/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.turbopack.tsx
index 6866c1cd5c..246310c7a5 100644
--- a/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.turbopack.tsx
+++ b/keep-ui/shared/ui/MonacoYAMLEditor/editor.client.turbopack.tsx
@@ -4,9 +4,9 @@
 
 import { useEffect, useState } from "react";
 import { configureMonacoYaml, MonacoYaml } from "monaco-yaml";
-import { Editor, loader, type EditorProps } from "@monaco-editor/react";
+import { DiffEditor, Editor, loader } from "@monaco-editor/react";
 import { KeepLoader, ErrorComponent } from "@/shared/ui";
-
+import { MonacoYamlEditorProps } from "./MonacoYAMLEditor.types";
 const Loader = <KeepLoader loadingText="Loading Code Editor ..." />;
 
 loader.config({
@@ -39,20 +39,14 @@ self.MonacoEnvironment = {
   },
 };
 
-type MonacoYamlEditorProps = {
-  schemas: {
-    fileMatch: string[];
-    schema: object;
-    uri: string;
-  }[];
-} & EditorProps;
-
 /**
  * This is a custom editor component that uses 'monaco-yaml' to provide YAML language support.
  * It is used to edit YAML files.
  */
 export function MonacoYAMLEditorTurbopack({
   schemas,
+  original,
+  modified,
   ...props
 }: MonacoYamlEditorProps) {
   const [error, setError] = useState<Error | null>(null);
@@ -98,7 +92,18 @@ export function MonacoYAMLEditorTurbopack({
     );
   }
 
-  return <Editor defaultLanguage="yaml" loading={Loader} {...props} />;
+  if (original && modified) {
+    return (
+      // @ts-expect-error - DiffEditorProps is not typed correctly yet
+      <DiffEditor
+        original={original}
+        modified={modified}
+        language="yaml"
+        loading={Loader}
+        {...props}
+      />
+    );
+  }
+
+  return <Editor language="yaml" loading={Loader} {...props} />;
 }
-// mv keep-ui/shared/ui/MonacoYAMLEditor keep-ui/shared/ui/MonacoYAMLEditor_temp && git rm --cached -r keep-ui/shared/ui/MonacoYAMLEditor 2>/dev/null || true
-// mv keep-ui/shared/ui/MonacoYAMLEditor_temp keep-ui/shared/ui/MonacoYAMLEditor && git add keep-ui/shared/ui/MonacoYAMLEditor/*
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/index.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/index.ts
index 548ba2a315..11e9b19b6c 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/index.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/index.ts
@@ -1 +1,7 @@
 export { WorkflowYAMLEditor } from "./ui/WorkflowYAMLEditor";
+export type {
+  WorkflowYAMLEditorDefaultProps,
+  WorkflowYAMLEditorDiffProps,
+  WorkflowYAMLEditorProps,
+} from "./model/types";
+export { isDiffEditorProps } from "./lib/utils";
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
index e67f49cf49..a817d9897d 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
@@ -1,4 +1,8 @@
-import { YamlValidationErrorSeverity } from "../model/types";
+import {
+  WorkflowYAMLEditorDiffProps,
+  WorkflowYAMLEditorProps,
+  YamlValidationErrorSeverity,
+} from "../model/types";
 
 // Copied from monaco-editor/esm/vs/editor/editor.api.d.ts because we can't import with turbopack
 export enum MarkerSeverity {
@@ -24,9 +28,16 @@ export function getSeverityString(
   }
 }
 
-// New utility function to handle error click position
+export function isDiffEditorProps(
+  props: WorkflowYAMLEditorProps
+): props is WorkflowYAMLEditorDiffProps {
+  return "original" in props && "modified" in props;
+}
+
 export function navigateToErrorPosition(
-  editor: import("monaco-editor").editor.IStandaloneCodeEditor,
+  editor:
+    | import("monaco-editor").editor.IStandaloneCodeEditor
+    | import("monaco-editor").editor.IDiffEditor,
   lineNumber: number,
   column: number
 ): void {
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
index d93ad2f56e..178cea86d2 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
@@ -1,3 +1,5 @@
+import type { editor } from "monaco-editor";
+
 export type YamlValidationErrorSeverity = "error" | "warning" | "info" | "hint";
 
 export type YamlValidationError = {
@@ -7,3 +9,32 @@ export type YamlValidationError = {
   column: number;
   owner: string;
 };
+
+export interface BaseWorkflowYAMLEditorProps {
+  workflowId?: string;
+  filename?: string;
+  readOnly?: boolean;
+  "data-testid"?: string;
+  onMount?: (
+    editor: editor.IStandaloneCodeEditor,
+    monacoInstance: typeof import("monaco-editor")
+  ) => void;
+  onChange?: (value: string | undefined) => void;
+  onValidationErrors?: React.Dispatch<
+    React.SetStateAction<YamlValidationError[]>
+  >;
+  onSave?: (value: string) => void;
+}
+
+export type WorkflowYAMLEditorDefaultProps = BaseWorkflowYAMLEditorProps & {
+  value: string;
+};
+
+export type WorkflowYAMLEditorDiffProps = BaseWorkflowYAMLEditorProps & {
+  original: string;
+  modified: string;
+};
+
+export type WorkflowYAMLEditorProps =
+  | WorkflowYAMLEditorDefaultProps
+  | WorkflowYAMLEditorDiffProps;
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditor.tsx b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditor.tsx
index cccf6c752c..7833995527 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditor.tsx
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditor.tsx
@@ -9,12 +9,12 @@ import React, {
   useEffect,
 } from "react";
 import type { editor } from "monaco-editor";
+import { useWorkflowJsonSchema } from "@/entities/workflows/lib/useWorkflowJsonSchema";
+import { WorkflowYAMLEditorProps } from "../model/types";
 // NOTE: IT IS IMPORTANT TO IMPORT MonacoYAMLEditor FROM THE SHARED UI DIRECTORY, because import will be replaced for turbopack
 import { MonacoYAMLEditor, KeepLoader } from "@/shared/ui";
 import { downloadFileFromString } from "@/shared/lib/downloadFileFromString";
 import { WorkflowYAMLValidationErrors } from "./WorkflowYAMLValidationErrors";
-import { useWorkflowJsonSchema } from "@/entities/workflows/lib/useWorkflowJsonSchema";
-import { YamlValidationError } from "../model/types";
 import { useYamlValidation } from "../lib/useYamlValidation";
 import { WorkflowYAMLEditorToolbar } from "./WorkflowYAMLEditorToolbar";
 import { navigateToErrorPosition } from "../lib/utils";
@@ -22,26 +22,8 @@ import { useWorkflowSecrets } from "@/utils/hooks/useWorkflowSecrets";
 
 const KeepSchemaPath = "file:///workflow-schema.json";
 
-export interface WorkflowYAMLEditorProps {
-  workflowYamlString: string;
-  workflowId?: string;
-  filename?: string;
-  readOnly?: boolean;
-  "data-testid"?: string;
-  onMount?: (
-    editor: editor.IStandaloneCodeEditor,
-    monacoInstance: typeof import("monaco-editor")
-  ) => void;
-  onChange?: (value: string | undefined) => void;
-  onValidationErrors?: React.Dispatch<
-    React.SetStateAction<YamlValidationError[]>
-  >;
-  onSave?: (value: string) => void;
-}
-
 export const WorkflowYAMLEditor = ({
   workflowId,
-  workflowYamlString,
   filename = "workflow",
   readOnly = false,
   "data-testid": dataTestId = "yaml-editor",
@@ -49,9 +31,12 @@ export const WorkflowYAMLEditor = ({
   onChange,
   onSave,
   onValidationErrors,
+  ...props
 }: WorkflowYAMLEditorProps) => {
   const monacoRef = useRef<typeof import("monaco-editor") | null>(null);
-  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
+  const editorRef = useRef<
+    editor.IStandaloneCodeEditor | editor.IDiffEditor | null
+  >(null);
   const { getSecrets } = useWorkflowSecrets(workflowId);
   const { data: secrets } = getSecrets;
 
@@ -76,20 +61,51 @@ export const WorkflowYAMLEditor = ({
 
   const [isEditorMounted, setIsEditorMounted] = useState(false);
 
-  const handleChange = useCallback(
-    (value: string | undefined) => {
-      if (onChange) {
-        onChange(value);
+  const getEditorValue = useCallback(() => {
+    if (!editorRef.current) {
+      return;
+    }
+    const model = editorRef.current.getModel();
+    if (!model) {
+      return;
+    }
+    if ("original" in model) {
+      return model.modified.getValue();
+    }
+    return model.getValue();
+  }, []);
+
+  const validateMustacheExpressionsEverywhere = useCallback(() => {
+    if (editorRef.current && monacoRef.current) {
+      const model = editorRef.current.getModel();
+      if (!model) {
+        return;
       }
-      if (editorRef.current && monacoRef.current) {
+      if ("original" in model) {
+        validateMustacheExpressions(
+          model.original,
+          monacoRef.current,
+          secrets ?? {}
+        );
         validateMustacheExpressions(
-          editorRef.current.getModel(),
+          model.modified,
           monacoRef.current,
           secrets ?? {}
         );
+      } else {
+        validateMustacheExpressions(model, monacoRef.current, secrets ?? {});
+      }
+    }
+  }, [validateMustacheExpressions, secrets]);
+
+  const handleChange = useCallback(
+    (value: string | undefined) => {
+      if (onChange) {
+        onChange(value);
       }
+      validateMustacheExpressionsEverywhere();
     },
-    [onChange, validateMustacheExpressions]
+    [onChange, validateMustacheExpressionsEverywhere]
   );
 
   const handleEditorDidMount = (
@@ -119,40 +135,40 @@ export const WorkflowYAMLEditor = ({
   useEffect(() => {
     // After editor is mounted, validate the initial content
     if (isEditorMounted && editorRef.current && monacoRef.current) {
-      validateMustacheExpressions(
-        editorRef.current.getModel(),
-        monacoRef.current,
-        secrets ?? {}
-      );
+      validateMustacheExpressionsEverywhere();
     }
-  }, [validateMustacheExpressions, isEditorMounted]);
+  }, [validateMustacheExpressionsEverywhere, isEditorMounted]);
 
   const downloadYaml = useCallback(() => {
-    if (!editorRef.current) {
+    const value = getEditorValue();
+    if (!value) {
       return;
     }
     downloadFileFromString({
-      data: editorRef.current.getValue(),
+      data: value,
       filename: `${filename}.yaml`,
       contentType: "text/yaml",
     });
   }, [filename]);
 
   const copyToClipboard = useCallback(async () => {
-    if (!editorRef.current) {
+    const value = getEditorValue();
+    if (!value) {
       return;
     }
-    const content = editorRef.current.getValue();
     try {
-      await navigator.clipboard.writeText(content);
+      await navigator.clipboard.writeText(value);
     } catch (err) {
       console.error("Failed to copy text:", err);
     }
   }, []);
 
   const handleSave = useCallback(() => {
-    if (!editorRef.current || !onSave) return;
-    onSave(editorRef.current.getValue());
+    const value = getEditorValue();
+    if (!onSave || !value) {
+      return;
+    }
+    onSave(value);
   }, [onSave]);
 
   const editorOptions = useMemo<editor.IStandaloneEditorConstructionOptions>(
@@ -164,6 +180,7 @@ export const WorkflowYAMLEditor = ({
       scrollBeyondLastLine: false,
       automaticLayout: true,
       tabSize: 2,
+      lineNumbersMinChars: 2,
       insertSpaces: true,
       fontSize: 14,
       renderWhitespace: "all",
@@ -201,13 +218,13 @@ export const WorkflowYAMLEditor = ({
             height="100%"
             className="[&_.monaco-editor]:outline-none [&_.decorationsOverviewRuler]:z-2"
             wrapperProps={{ "data-testid": dataTestId }}
-            value={workflowYamlString}
             onMount={handleEditorDidMount}
             onChange={handleChange}
             options={editorOptions}
             loading={<KeepLoader loadingText="Loading YAML editor..." />}
             theme="light"
             schemas={schemas}
+            {...props}
           />
         </Suspense>
       </div>
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
index a2f2ebc493..83b038e65c 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
@@ -133,7 +133,7 @@ export function WorkflowYAMLEditorStandalone({
         </Button>
       </WorkflowYamlEditorHeader>
       <WorkflowYAMLEditor
-        workflowYamlString={yamlString}
+        value={yamlString}
         filename={workflowId ?? "workflow"}
         workflowId={workflowId}
         onMount={handleEditorDidMount}
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditorWithLogs/WorkflowYAMLEditorWithLogs.tsx b/keep-ui/shared/ui/WorkflowYAMLEditorWithLogs/WorkflowYAMLEditorWithLogs.tsx
index 87976da22d..ae9c8445cd 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditorWithLogs/WorkflowYAMLEditorWithLogs.tsx
+++ b/keep-ui/shared/ui/WorkflowYAMLEditorWithLogs/WorkflowYAMLEditorWithLogs.tsx
@@ -1,23 +1,24 @@
 "use client";
 import { LogEntry } from "@/shared/api/workflow-executions";
 import { type editor } from "monaco-editor";
-import {
-  WorkflowYAMLEditor,
-  WorkflowYAMLEditorProps,
-} from "../WorkflowYAMLEditor/ui/WorkflowYAMLEditor";
+import { WorkflowYAMLEditor } from "@/shared/ui";
 import { useCallback, useEffect, useMemo, useRef } from "react";
 import { getStepStatus } from "@/shared/lib/logs-utils";
-import "./WorkflowYAMLEditorWithLogs.css";
 import { getOrderedWorkflowYamlString } from "@/entities/workflows/lib/yaml-utils";
+import {
+  WorkflowYAMLEditorProps,
+  isDiffEditorProps,
+} from "@/shared/ui/WorkflowYAMLEditor";
+import "./WorkflowYAMLEditorWithLogs.css";
 
-interface WorkflowYAMLEditorWithLogsProps extends WorkflowYAMLEditorProps {
+type WorkflowYAMLEditorWithLogsProps = WorkflowYAMLEditorProps & {
   executionLogs: LogEntry[] | null | undefined;
   executionStatus: string | null | undefined;
   hoveredStep: string | null | undefined;
   setHoveredStep: (step: string | null) => void;
   selectedStep: string | null | undefined;
   setSelectedStep: (step: string | null) => void;
-}
+};
 
 // TODO: refactor this to use yaml AST instead of string manipulation
 export function WorkflowYAMLEditorWithLogs({
@@ -27,7 +28,6 @@ export function WorkflowYAMLEditorWithLogs({
   setHoveredStep,
   selectedStep,
   setSelectedStep,
-  workflowYamlString,
   ...props
 }: WorkflowYAMLEditorWithLogsProps) {
   const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
@@ -36,8 +36,11 @@ export function WorkflowYAMLEditorWithLogs({
   const hoverDecorationsRef = useRef<string[]>([]);
 
   const orderedWorkflowYamlString = useMemo(() => {
-    return getOrderedWorkflowYamlString(workflowYamlString);
-  }, [workflowYamlString]);
+    if (isDiffEditorProps(props)) {
+      return getOrderedWorkflowYamlString(props.modified);
+    }
+    return getOrderedWorkflowYamlString(props.value);
+  }, [props]);
 
   const findStepNameForPosition = (
     lineNumber: number,
@@ -375,7 +378,7 @@ export function WorkflowYAMLEditorWithLogs({
   return (
     <WorkflowYAMLEditor
       onMount={handleEditorDidMount}
-      workflowYamlString={orderedWorkflowYamlString}
+      value={orderedWorkflowYamlString}
       {...props}
     />
   );
diff --git a/keep-ui/shared/ui/index.ts b/keep-ui/shared/ui/index.ts
index 07d1ed85e9..330c802956 100644
--- a/keep-ui/shared/ui/index.ts
+++ b/keep-ui/shared/ui/index.ts
@@ -27,4 +27,6 @@ export { WorkflowYAMLEditor } from "./WorkflowYAMLEditor";
 export { showErrorToast } from "./utils/showErrorToast";
 export { showSuccessToast } from "./utils/showSuccessToast";
 export { DebugJSON } from "./DebugJSON";
+export { getIconForStatusString } from "./utils/getIconForStatusString";
+
 export type { UISeverity } from "./utils/severity-utils";
diff --git a/keep-ui/shared/ui/utils/getIconForStatusString.tsx b/keep-ui/shared/ui/utils/getIconForStatusString.tsx
new file mode 100644
index 0000000000..4c71c19021
--- /dev/null
+++ b/keep-ui/shared/ui/utils/getIconForStatusString.tsx
@@ -0,0 +1,32 @@
+import {
+  CheckCircleIcon,
+  NoSymbolIcon,
+  XCircleIcon,
+} from "@heroicons/react/20/solid";
+
+export function getIconForStatusString(status: string) {
+  let icon;
+  switch (status) {
+    case "success":
+      icon = <CheckCircleIcon className="size-6 cover text-green-500" />;
+      break;
+    case "skipped":
+      icon = (
+        <NoSymbolIcon className="size-6 cover text-slate-500" title="Skipped" />
+      );
+      break;
+    case "failed":
+    case "fail":
+    case "failure":
+    case "error":
+    case "timeout":
+      icon = <XCircleIcon className="size-6 cover text-red-500" />;
+      break;
+    case "in_progress":
+      icon = <div className="loader"></div>;
+      break;
+    default:
+      icon = <div className="loader"></div>;
+  }
+  return icon;
+}
diff --git a/keep-ui/widgets/workflow-builder/workflow-builder.tsx b/keep-ui/widgets/workflow-builder/workflow-builder.tsx
index 027da84b75..effa720c39 100644
--- a/keep-ui/widgets/workflow-builder/workflow-builder.tsx
+++ b/keep-ui/widgets/workflow-builder/workflow-builder.tsx
@@ -260,7 +260,7 @@ export function WorkflowBuilder({
           )}
         >
           <WorkflowYAMLEditor
-            workflowYamlString={workflowYaml ?? ""}
+            value={workflowYaml ?? ""}
             filename={workflowId ?? "workflow"}
             workflowId={workflowId}
             data-testid="wf-builder-yaml-editor"
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 8595d22972..2f42623985 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -188,6 +188,7 @@ def wrapper(*args, **kwargs):
 
 def create_workflow_execution(
     workflow_id: str,
+    workflow_revision: int,
     tenant_id: str,
     triggered_by: str,
     execution_number: int = 1,
@@ -207,6 +208,7 @@ def create_workflow_execution(
             workflow_execution = WorkflowExecution(
                 id=workflow_execution_id,
                 workflow_id=workflow_id if not test_run else "test",
+                workflow_revision=workflow_revision,
                 tenant_id=tenant_id,
                 started=datetime.now(tz=timezone.utc),
                 triggered_by=triggered_by,
@@ -310,6 +312,7 @@ def get_workflows_that_should_run():
         try:
             result = session.exec(
                 select(Workflow)
+                .filter(Workflow.is_latest == True)
                 .filter(Workflow.is_deleted == False)
                 .filter(Workflow.is_disabled == False)
                 .filter(Workflow.interval != None)
@@ -330,7 +333,7 @@ def get_workflows_that_should_run():
                 try:
                     # try to get the lock
                     workflow_execution_id = create_workflow_execution(
-                        workflow.id, workflow.tenant_id, "scheduler"
+                        workflow.id, workflow.revision, workflow.tenant_id, "scheduler"
                     )
                     # we succeed to get the lock on this execution number :)
                     # let's run it
@@ -353,6 +356,7 @@ def get_workflows_that_should_run():
                     # try to get the lock with execution_number + 1
                     workflow_execution_id = create_workflow_execution(
                         workflow.id,
+                        workflow.revision,
                         workflow.tenant_id,
                         "scheduler",
                         last_execution.execution_number + 1,
@@ -404,6 +408,7 @@ def get_workflows_that_should_run():
                     try:
                         workflow_execution_id = create_workflow_execution(
                             workflow.id,
+                            workflow.revision,
                             workflow.tenant_id,
                             "scheduler",
                             ongoing_execution.execution_number + 1,
@@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
+    with Session(engine, expire_on_commit=False) as session:
+        # TODO: ensure that id is unique to workflow_id, like if rolled back and now updating from this version it should be bigger than latest version
+        next_revision = existing_workflow.revision + 1
+        updated_by_str = updated_by or existing_workflow.updated_by or "unknown"
+        # update version
+        version = WorkflowVersion(
+            workflow_id=existing_workflow.id,
+            revision=next_revision,
+            workflow_raw=workflow_raw,
+            updated_by=updated_by_str,
+            comment=f"Updated by {updated_by_str}",
+            # TODO: check if valid
+            is_valid=True,
+            is_current=True,
+        )
+        session.add(version)
+
+        existing_version = session.exec(
+            select(WorkflowVersion)
+            .where(WorkflowVersion.workflow_id == existing_workflow.id)
+            .where(WorkflowVersion.revision == existing_workflow.revision)
+        ).first()
+        if existing_version:
+            existing_version.is_current = False
+            session.add(existing_version)
+
+        # tb: no need to override the id field here because it has foreign key constraints.
+        existing_workflow.name = name
+        existing_workflow.description = description
+        existing_workflow.updated_by = updated_by_str
+        existing_workflow.interval = interval
+        existing_workflow.workflow_raw = workflow_raw
+        existing_workflow.revision = next_revision
+        existing_workflow.last_updated = datetime.now()  # Update last_updated
+        existing_workflow.is_deleted = False
+        existing_workflow.is_disabled = is_disabled
+        existing_workflow.provisioned = provisioned
+        existing_workflow.provisioned_file = provisioned_file
+        session.add(existing_workflow)
+        session.commit()
+        return existing_workflow
+
+
 def add_or_update_workflow(
     id,
     name,
@@ -445,33 +538,26 @@ def add_or_update_workflow(
 ) -> Workflow:
     with Session(engine, expire_on_commit=False) as session:
         # TODO: we need to better understanad if that's the right behavior we want
-        existing_workflow = (
-            session.query(Workflow)
-            .filter_by(name=name)
-            .filter_by(tenant_id=tenant_id)
-            .first()
-        )
+        existing_workflow = get_workflow(tenant_id, id)
 
         if existing_workflow:
-            # tb: no need to override the id field here because it has foreign key constraints.
-            existing_workflow.tenant_id = tenant_id
-            existing_workflow.description = description
-            existing_workflow.updated_by = (
-                updated_by or existing_workflow.updated_by
-            )  # Update the updated_by field if provided
-            existing_workflow.interval = interval
-            existing_workflow.workflow_raw = workflow_raw
-            existing_workflow.revision += 1  # Increment the revision
-            existing_workflow.last_updated = datetime.now()  # Update last_updated
-            existing_workflow.is_deleted = False
-            existing_workflow.is_disabled = is_disabled
-            existing_workflow.provisioned = provisioned
-            existing_workflow.provisioned_file = provisioned_file
+            return update_workflow_by_workflow(
+                existing_workflow,
+                name=name,
+                description=description,
+                interval=interval,
+                workflow_raw=workflow_raw,
+                is_disabled=is_disabled,
+                provisioned=provisioned,
+                provisioned_file=provisioned_file,
+                updated_by=updated_by,
+            )
 
         else:
             # Create a new workflow
             workflow = Workflow(
                 id=id,
+                revision=1,
                 name=name,
                 tenant_id=tenant_id,
                 description=description,
@@ -484,9 +570,8 @@ def add_or_update_workflow(
                 provisioned_file=provisioned_file,
             )
             session.add(workflow)
-
-        session.commit()
-        return existing_workflow if existing_workflow else workflow
+            session.commit()
+            return workflow
 
 
 def get_workflow_to_alert_execution_by_workflow_execution_id(
@@ -613,6 +698,7 @@ def get_workflows_with_last_execution(tenant_id: str) -> List[dict]:
                     == latest_execution_cte.c.last_execution_time,
                 ),
             )
+            .where(Workflow.is_latest == True)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.is_deleted == False)
         ).distinct()
@@ -621,25 +707,27 @@ def get_workflows_with_last_execution(tenant_id: str) -> List[dict]:
     return result
 
 
-def get_all_workflows(tenant_id: str) -> List[Workflow]:
+def get_all_workflows(tenant_id: str):
     with Session(engine) as session:
         workflows = session.exec(
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
+            .where(Workflow.is_latest == True)
             .where(Workflow.is_deleted == False)
         ).all()
     return workflows
 
 
-def get_all_provisioned_workflows(tenant_id: str) -> List[Workflow]:
+def get_all_provisioned_workflows(tenant_id: str):
     with Session(engine) as session:
         workflows = session.exec(
             select(Workflow)
+            .where(Workflow.is_latest == True)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.provisioned == True)
             .where(Workflow.is_deleted == False)
         ).all()
-    return workflows
+    return list(workflows)
 
 
 def get_all_provisioned_providers(tenant_id: str) -> List[Provider]:
@@ -649,46 +737,57 @@ def get_all_provisioned_providers(tenant_id: str) -> List[Provider]:
             .where(Provider.tenant_id == tenant_id)
             .where(Provider.provisioned == True)
         ).all()
-    return providers
+    return list(providers)
 
 
-def get_all_workflows_yamls(tenant_id: str) -> List[str]:
+def get_all_workflows_yamls(tenant_id: str):
     with Session(engine) as session:
         workflows = session.exec(
             select(Workflow.workflow_raw)
             .where(Workflow.tenant_id == tenant_id)
+            .where(Workflow.is_latest == True)
             .where(Workflow.is_deleted == False)
         ).all()
-    return workflows
+    return list(workflows)
 
 
-def get_workflow(tenant_id: str, workflow_id: str) -> Workflow | None:
+def get_workflow(tenant_id: str, workflow_id: str):
     with Session(engine) as session:
-        # if the workflow id is uuid:
+        query = (
+            select(Workflow)
+            .where(Workflow.tenant_id == tenant_id)
+            .where(Workflow.is_deleted == False)
+        )
         if validators.uuid(workflow_id):
-            workflow = session.exec(
-                select(Workflow)
-                .where(Workflow.tenant_id == tenant_id)
-                .where(Workflow.id == workflow_id)
-                .where(Workflow.is_deleted == False)
-            ).first()
+            query = query.where(Workflow.id == workflow_id)
         else:
-            workflow = session.exec(
-                select(Workflow)
-                .where(Workflow.tenant_id == tenant_id)
-                .where(Workflow.name == workflow_id)
-                .where(Workflow.is_deleted == False)
-            ).first()
-    if not workflow:
-        return None
+            query = query.where(Workflow.name == workflow_id)
+        workflow = session.exec(query).first()
     return workflow
 
 
-def get_raw_workflow(tenant_id: str, workflow_id: str) -> str:
-    workflow = get_workflow(tenant_id, workflow_id)
-    if not workflow:
-        return None
-    return workflow.workflow_raw
+def get_workflow_versions(tenant_id: str, workflow_id: str):
+    with Session(engine) as session:
+        versions = session.exec(
+            select(Workflow)
+            .where(Workflow.tenant_id == tenant_id)
+            .where(Workflow.id == workflow_id)
+            .where(Workflow.is_deleted == False)
+            .order_by(Workflow.revision.desc())
+        ).all()
+    return versions
+
+
+def get_workflow_version(tenant_id: str, workflow_id: str, revision: int):
+    with Session(engine) as session:
+        version = session.exec(
+            select(WorkflowVersion)
+            .where(WorkflowVersion.workflow_id == workflow_id)
+            .where(WorkflowVersion.revision == revision)
+            .where(WorkflowVersion.workflow.tenant_id == tenant_id)
+            .where(WorkflowVersion.workflow.is_deleted == False)
+        ).first()
+    return version
 
 
 def update_provider_last_pull_time(tenant_id: str, provider_id: str):
@@ -857,6 +956,7 @@ def delete_workflow(tenant_id, workflow_id):
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.id == workflow_id)
+            .where(Workflow.is_latest == True)
         ).first()
 
         if workflow:
@@ -883,6 +983,7 @@ def get_workflow_id(tenant_id, workflow_name):
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.name == workflow_name)
+            .where(Workflow.is_latest == True)
             .where(Workflow.is_deleted == False)
         ).first()
 
@@ -1885,6 +1986,7 @@ def get_workflow_by_name(tenant_id, workflow_name):
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.name == workflow_name)
+            .where(Workflow.is_latest == True)
             .where(Workflow.is_deleted == False)
         ).first()
 
@@ -3679,18 +3781,23 @@ def get_incident_by_id(
     if isinstance(incident_id, str):
         incident_id = __convert_to_uuid(incident_id, should_raise=True)
     with existed_or_new_session(session) as session:
-        query = session.query(
-            Incident,
-            AlertEnrichment,
-        ).outerjoin(
-            AlertEnrichment,
-            and_(
-                Incident.tenant_id == AlertEnrichment.tenant_id,
-                cast(col(Incident.id), String) == foreign(AlertEnrichment.alert_fingerprint),
-            ),
-        ).filter(
-            Incident.tenant_id == tenant_id,
-            Incident.id == incident_id,
+        query = (
+            session.query(
+                Incident,
+                AlertEnrichment,
+            )
+            .outerjoin(
+                AlertEnrichment,
+                and_(
+                    Incident.tenant_id == AlertEnrichment.tenant_id,
+                    cast(col(Incident.id), String)
+                    == foreign(AlertEnrichment.alert_fingerprint),
+                ),
+            )
+            .filter(
+                Incident.tenant_id == tenant_id,
+                Incident.id == incident_id,
+            )
         )
         incident_with_enrichments = query.first()
         if incident_with_enrichments:
diff --git a/keep/api/core/workflows.py b/keep/api/core/workflows.py
index 90e96aa424..09a3ba9a3e 100644
--- a/keep/api/core/workflows.py
+++ b/keep/api/core/workflows.py
@@ -142,6 +142,7 @@ def __build_base_query(tenant_id: str):
             ),
         )
         .where(Workflow.tenant_id == tenant_id)
+        .where(Workflow.is_latest == True)
         .where(Workflow.is_deleted == False)
     )
 
diff --git a/keep/api/models/db/migrations/utils.py b/keep/api/models/db/migrations/utils.py
new file mode 100644
index 0000000000..6e0a0b391d
--- /dev/null
+++ b/keep/api/models/db/migrations/utils.py
@@ -0,0 +1,26 @@
+from alembic import op
+import sqlalchemy as sa
+
+
+def drop_foreign_key_constraint(table_name, column_name, referred_table):
+    # First check if the column is nullable (for those who haven't migrated yet)
+    connection = op.get_bind()
+    inspector = sa.inspect(connection)
+
+    # Find the actual foreign key constraint name for workflow_id
+    foreign_keys = inspector.get_foreign_keys(table_name)
+    foreign_key = None
+    for fk in foreign_keys:
+        if (
+            column_name in fk.get("constrained_columns", [])
+            and fk.get("referred_table") == referred_table
+        ):
+            foreign_key = fk
+            break
+
+    fk_name = foreign_key.get("name") if foreign_key else None
+
+    # Drop the foreign key constraint if it exists
+    if fk_name:
+        with op.batch_alter_table(table_name, schema=None) as batch_op:
+            batch_op.drop_constraint(fk_name, type_="foreignkey")
diff --git a/keep/api/models/db/migrations/versions/2025-04-15-15-30_885ff6b12fed.py b/keep/api/models/db/migrations/versions/2025-04-15-15-30_885ff6b12fed.py
new file mode 100644
index 0000000000..91faef1be5
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2025-04-15-15-30_885ff6b12fed.py
@@ -0,0 +1,108 @@
+"""Workflow Versions
+
+Revision ID: 885ff6b12fed
+Revises: 59991b568c7d
+Create Date: 2025-04-15 15:30:48.099088
+
+"""
+
+import sqlalchemy as sa
+import sqlmodel
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "885ff6b12fed"
+down_revision = "59991b568c7d"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+
+    connection = op.get_bind()
+    dialect = connection.dialect.name
+    op.create_table(
+        "workflowversion",
+        sa.Column("workflow_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("revision", sa.Integer(), nullable=False),
+        sa.Column("workflow_raw", sa.TEXT(), nullable=True),
+        sa.Column("updated_by", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("updated_at", sa.DateTime(), nullable=False),
+        sa.Column("is_valid", sa.Boolean(), nullable=False),
+        sa.Column("is_current", sa.Boolean(), nullable=False),
+        sa.Column("comment", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["workflow_id"],
+            ["workflow.id"],
+        ),
+        sa.PrimaryKeyConstraint("workflow_id", "revision"),
+        sa.UniqueConstraint(
+            "workflow_id", "is_current", name="uq_workflow_current_version"
+        ),
+    )
+    with op.batch_alter_table("workflowexecution", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column("workflow_revision", sa.Integer(), nullable=False)
+        )
+        batch_op.drop_index("idx_workflowexecution_tenant_workflow_id_timestamp")
+        batch_op.create_index(
+            "idx_workflowexecution_tenant_workflow_id_timestamp",
+            ["tenant_id", "workflow_id", "workflow_revision", "started"],
+            unique=False,
+        )
+        batch_op.drop_index(
+            "idx_workflowexecution_workflow_tenant_started_status",
+            mysql_length={"status": 255},
+        )
+        batch_op.create_index(
+            "idx_workflowexecution_workflow_tenant_started_status",
+            [
+                "workflow_id",
+                "workflow_revision",
+                "tenant_id",
+                "started",
+                sa.text("status(255)") if dialect == "mysql" else "status",
+            ],
+            unique=False,
+        )
+        batch_op.create_index(
+            "idx_workflowexecution_workflow_revision",
+            ["workflow_id", "workflow_revision"],
+            unique=False,
+        )
+        batch_op.drop_constraint("workflowexecution_ibfk_2", type_="foreignkey")
+        batch_op.create_foreign_key(None, "workflow", ["workflow_id"], ["id"])
+
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table("workflowexecution", schema=None) as batch_op:
+        batch_op.drop_constraint(None, type_="foreignkey")
+        batch_op.create_foreign_key(
+            "workflowexecution_ibfk_2",
+            "workflow",
+            ["workflow_id"],
+            ["id"],
+            ondelete="SET DEFAULT",
+        )
+        batch_op.drop_index("idx_workflowexecution_workflow_revision")
+        batch_op.drop_index("idx_workflowexecution_workflow_tenant_started_status")
+        batch_op.create_index(
+            "idx_workflowexecution_workflow_tenant_started_status",
+            ["workflow_id", "tenant_id", "started", "status"],
+            unique=False,
+            mysql_length={"status": 255},
+        )
+        batch_op.drop_index("idx_workflowexecution_tenant_workflow_id_timestamp")
+        batch_op.create_index(
+            "idx_workflowexecution_tenant_workflow_id_timestamp",
+            ["tenant_id", "workflow_id", "started"],
+            unique=False,
+        )
+        batch_op.drop_column("workflow_revision")
+
+    op.drop_table("workflowversion")
+    # ### end Alembic commands ###
diff --git a/keep/api/models/db/workflow.py b/keep/api/models/db/workflow.py
index 6ed19e71ae..5200dd2f6a 100644
--- a/keep/api/models/db/workflow.py
+++ b/keep/api/models/db/workflow.py
@@ -3,7 +3,7 @@
 from typing import List, Optional
 
 import sqlalchemy
-from sqlalchemy import TEXT, Index
+from sqlalchemy import TEXT, Index, PrimaryKeyConstraint
 from sqlmodel import JSON, Column, Field, Relationship, SQLModel, UniqueConstraint
 
 
@@ -28,6 +28,27 @@ class Config:
         orm_mode = True
 
 
+class WorkflowVersion(SQLModel, table=True):
+    __table_args__ = (
+        PrimaryKeyConstraint("workflow_id", "revision"),
+        # Add unique constraint to ensure only one current version per workflow
+        UniqueConstraint(
+            "workflow_id", "is_current", name="uq_workflow_current_version"
+        ),
+    )
+
+    workflow_id: str = Field(primary_key=True, foreign_key="workflow.id")
+    revision: int = Field(primary_key=True)
+    workflow_raw: str = Field(sa_column=Column(TEXT))
+    updated_by: str
+    updated_at: datetime = Field(default_factory=datetime.utcnow)
+    is_valid: bool = Field(default=False)
+    is_current: bool = Field(default=False)
+    comment: Optional[str] = None
+
+    workflow: "Workflow" = Relationship(back_populates="versions")
+
+
 def get_status_column():
     try:
         db_connection_string = os.environ.get(
@@ -54,21 +75,36 @@ class WorkflowExecution(SQLModel, table=True):
             "idx_workflowexecution_tenant_workflow_id_timestamp",
             "tenant_id",
             "workflow_id",
+            "workflow_revision",
             "started",
         ),
         Index(
             "idx_workflowexecution_workflow_tenant_started_status",
             "workflow_id",
+            "workflow_revision",
             "tenant_id",
             "started",
             get_status_column(),
         ),
+        Index(
+            "idx_status_started",
+            "status",
+            "started",
+        ),
+        Index(
+            "idx_workflowexecution_workflow_revision",
+            "workflow_id",
+            "workflow_revision",
+        ),
     )
 
     id: str = Field(default=None, primary_key=True)
     workflow_id: str = Field(
         foreign_key="workflow.id", default="test"
     )  # default=test for test runs, which are not associated with a workflow
+    workflow_revision: int = Field(
+        default=1
+    )  # Add this to track which version was executed
     tenant_id: str = Field(foreign_key="tenant.id")
     started: datetime = Field(default_factory=datetime.utcnow, index=True)
     triggered_by: str = Field(sa_column=Column(TEXT))
@@ -82,6 +118,13 @@ class WorkflowExecution(SQLModel, table=True):
     execution_time: Optional[int]
     results: dict = Field(sa_column=Column(JSON), default={})
 
+    version: "WorkflowVersion" = Relationship(
+        back_populates="workflow_execution",
+        sa_relationship_kwargs={
+            "foreign_keys": "[WorkflowExecution.workflow_id, WorkflowExecution.workflow_revision]"
+        },
+    )
+
     logs: List["WorkflowExecutionLog"] = Relationship(
         back_populates="workflowexecution"
     )
diff --git a/keep/api/models/workflow.py b/keep/api/models/workflow.py
index f5a38858e4..c64348706f 100644
--- a/keep/api/models/workflow.py
+++ b/keep/api/models/workflow.py
@@ -37,6 +37,7 @@ class WorkflowDTO(BaseModel):
     workflow_raw: str
     revision: int = 1
     last_updated: datetime = None
+    last_updated_by: str = None
     invalid: bool = False  # whether the workflow is invalid or not (for UI purposes)
     last_executions: List[dict] = None
     last_execution_started: datetime = None
@@ -110,6 +111,7 @@ class WorkflowToAlertExecutionDTO(BaseModel):
 class WorkflowExecutionDTO(BaseModel):
     id: str
     workflow_id: str | None  # None for test runs
+    workflow_revision: int | None
     started: datetime
     triggered_by: str
     status: str
@@ -136,4 +138,11 @@ class WorkflowRawDto(BaseModel):
     workflow_raw: str
 
 
-# trigger CI. TODO: remove this
+class WorkflowVersionDTO(BaseModel):
+    revision: int
+    updated_by: str | None
+    last_updated: datetime
+
+
+class WorkflowVersionListDTO(BaseModel):
+    versions: List[WorkflowVersionDTO]
diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py
index 91563f9115..1c2a735cfd 100644
--- a/keep/api/routes/workflows.py
+++ b/keep/api/routes/workflows.py
@@ -27,6 +27,9 @@
     get_last_workflow_workflow_to_alert_executions,
     get_session,
     get_workflow,
+    get_workflow_version,
+    get_workflow_versions,
+    update_workflow_by_id as update_workflow_by_id_db,
 )
 from keep.api.core.db import get_workflow_executions as get_workflow_executions_db
 from keep.api.core.workflows import (
@@ -46,6 +49,8 @@
     WorkflowRawDto,
     WorkflowRunResponseDTO,
     WorkflowToAlertExecutionDTO,
+    WorkflowVersionDTO,
+    WorkflowVersionListDTO,
 )
 from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts
 from keep.api.utils.pagination import WorkflowExecutionsPaginatedResultsDto
@@ -388,7 +393,12 @@ def run_workflow(
             event, inputs = get_event_from_body(body, tenant_id)
 
         workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow(
-            workflow_id, tenant_id, created_by, event, inputs=inputs
+            workflow_id,
+            workflow.workflow_revision,
+            tenant_id,
+            created_by,
+            event,
+            inputs=inputs,
         )
     except Exception as e:
         logger.exception(
@@ -484,6 +494,7 @@ async def run_workflow_from_definition(
         event, inputs = get_event_from_body(body, tenant_id)
         workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow(
             workflow.workflow_id,
+            workflow.revision,
             tenant_id,
             created_by,
             event,
@@ -718,26 +729,23 @@ async def update_workflow_by_id(
     workflow_raw_data = await __get_workflow_raw_data(request, None)
     parser = Parser()
     workflow_interval = parser.parse_interval(workflow_raw_data)
-    # In case the workflow name changed to empty string, keep the old name
-    workflow_name = workflow_raw_data.get("name", "")
-    if workflow_name != "":
-        workflow_from_db.name = workflow_name
-    else:
-        workflow_raw_data["name"] = workflow_from_db.name
-    workflow_from_db.description = workflow_raw_data.get("description")
-    workflow_from_db.interval = workflow_interval
-    workflow_from_db.is_disabled = workflow_raw_data.get("disabled", False)
-    workflow_from_db.workflow_raw = cyaml.dump(workflow_raw_data, width=99999)
-    workflow_from_db.last_updated = datetime.datetime.now()
-    workflow_from_db.revision += 1
-    session.add(workflow_from_db)
-    session.commit()
-    session.refresh(workflow_from_db)
+    updated_workflow = update_workflow_by_id_db(
+        id=workflow_id,
+        tenant_id=tenant_id,
+        name=workflow_raw_data.get("name", ""),
+        description=workflow_raw_data.get("description"),
+        interval=workflow_interval,
+        workflow_raw=cyaml.dump(workflow_raw_data, width=99999),
+        updated_by=authenticated_entity.email,
+        is_disabled=workflow_raw_data.get("disabled", False),
+    )
     logger.info(f"Updated workflow {workflow_id}", extra={"tenant_id": tenant_id})
-    return WorkflowCreateOrUpdateDTO(workflow_id=workflow_id, status="updated")
+    return WorkflowCreateOrUpdateDTO(
+        workflow_id=workflow_id, revision=updated_workflow.revision, status="updated"
+    )
 
 
-@router.get("/{workflow_id}/raw", description="Get workflow executions by ID")
+@router.get("/{workflow_id}/raw", description="Get raw workflow by ID")
 def get_raw_workflow_by_id(
     workflow_id: str,
     authenticated_entity: AuthenticatedEntity = Depends(
@@ -754,8 +762,12 @@ def get_raw_workflow_by_id(
 
 
 @router.get("/{workflow_id}", description="Get workflow by ID")
+@router.get(
+    "/{workflow_id}/versions/{revision}", description="Get workflow by ID and revision"
+)
 def get_workflow_by_id(
     workflow_id: str,
+    revision: int | None = None,
     authenticated_entity: AuthenticatedEntity = Depends(
         IdentityManagerFactory.get_auth_verifier(["read:workflows"])
     ),
@@ -770,6 +782,20 @@ def get_workflow_by_id(
         )
         raise HTTPException(404, "Workflow not found")
 
+    updated_at = workflow.last_updated
+    updated_by = workflow.updated_by or "unknown"
+    workflow_raw = workflow.workflow_raw
+
+    if revision:
+        workflow_version = get_workflow_version(
+            tenant_id=tenant_id, workflow_id=workflow_id, revision=revision
+        )
+        if not workflow_version:
+            raise HTTPException(404, "Workflow version not found")
+        updated_at = workflow_version.last_updated
+        updated_by = workflow_version.updated_by or "unknown"
+        workflow_raw = workflow_version.workflow_raw
+
     installed_providers = get_installed_providers(tenant_id)
     installed_providers_by_type = {}
     for installed_provider in installed_providers:
@@ -794,28 +820,52 @@ def get_workflow_by_id(
         providers_dto, triggers = [], []  # Default in case of failure
 
     try:
-        workflow_yaml = cyaml.safe_load(workflow.workflow_raw)
+        workflow_yaml = cyaml.safe_load(workflow_raw)
         valid_workflow_yaml = {"workflow": workflow_yaml}
         final_workflow_raw = cyaml.dump(valid_workflow_yaml, width=99999)
-        workflow_dto = WorkflowDTO(
-            id=workflow.id,
-            name=workflow.name,
-            description=workflow.description or "[This workflow has no description]",
-            created_by=workflow.created_by,
-            creation_time=workflow.creation_time,
-            interval=workflow.interval,
-            providers=providers_dto,
-            triggers=triggers,
-            workflow_raw=final_workflow_raw,
-            last_updated=workflow.last_updated,
-            disabled=workflow.is_disabled,
-            revision=workflow.revision,
-        )
-        return workflow_dto
+
     except cyaml.YAMLError:
         logger.exception("Invalid YAML format")
         raise HTTPException(status_code=500, detail="Error fetching workflow meta data")
 
+    return WorkflowDTO(
+        id=workflow.id,
+        name=workflow.name,
+        description=workflow.description or "[This workflow has no description]",
+        created_by=workflow.created_by,
+        creation_time=workflow.creation_time,
+        interval=workflow.interval,
+        providers=providers_dto,
+        triggers=triggers,
+        workflow_raw=final_workflow_raw,
+        last_updated=updated_at,
+        disabled=workflow.is_disabled,
+        revision=workflow.revision,
+        last_updated_by=updated_by,
+    )
+
+
+@router.get("/{workflow_id}/versions")
+def list_workflow_versions(
+    workflow_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:workflows"])
+    ),
+):
+    tenant_id = authenticated_entity.tenant_id
+    versions = get_workflow_versions(tenant_id=tenant_id, workflow_id=workflow_id)
+
+    return WorkflowVersionListDTO(
+        versions=[
+            WorkflowVersionDTO(
+                revision=version.revision,
+                updated_by=version.updated_by,
+                last_updated=version.last_updated,
+            )
+            for version in versions
+        ]
+    )
+
 
 @router.get("/{workflow_id}/runs", description="Get workflow executions by ID")
 def get_workflow_runs_by_id(
@@ -832,6 +882,13 @@ def get_workflow_runs_by_id(
 ) -> WorkflowExecutionsPaginatedResultsDto:
     tenant_id = authenticated_entity.tenant_id
     workflow = get_workflow(tenant_id=tenant_id, workflow_id=workflow_id)
+    if not workflow:
+        logger.warning(
+            f"Tenant tried to get workflow {workflow_id} that does not exist",
+            extra={"tenant_id": tenant_id},
+        )
+        raise HTTPException(404, "Workflow not found")
+
     installed_providers = get_installed_providers(tenant_id)
     installed_providers_by_type = {}
     for installed_provider in installed_providers:
@@ -863,6 +920,7 @@ def get_workflow_runs_by_id(
             workflow_execution_dto = {
                 "id": workflow_execution.id,
                 "workflow_id": workflow_execution.workflow_id,
+                "workflow_revision": workflow_execution.workflow_revision,
                 "status": workflow_execution.status,
                 "started": workflow_execution.started.isoformat(),
                 "triggered_by": workflow_execution.triggered_by,
@@ -946,7 +1004,9 @@ def get_workflow_execution_status(
         )
 
     workflow = get_workflow(
-        tenant_id=tenant_id, workflow_id=workflow_execution.workflow_id
+        tenant_id=tenant_id,
+        workflow_id=workflow_execution.workflow_id,
+        revision=workflow_execution.workflow_revision,
     )
 
     event_id = None
@@ -964,6 +1024,7 @@ def get_workflow_execution_status(
         id=workflow_execution.id,
         workflow_name=workflow.name if workflow else None,
         workflow_id=workflow_execution.workflow_id,
+        workflow_revision=workflow_execution.workflow_revision,
         status=workflow_execution.status,
         started=workflow_execution.started,
         triggered_by=workflow_execution.triggered_by,
@@ -1025,6 +1086,7 @@ def toggle_workflow_state(
         raise HTTPException(403, detail="Cannot modify a provisioned workflow")
 
     # Toggle the disabled state
+    # TODO: update workflow_raw
     workflow.is_disabled = not workflow.is_disabled
     workflow.last_updated = datetime.datetime.now()
 
diff --git a/keep/parser/parser.py b/keep/parser/parser.py
index 8effddf26a..6bf748ad9e 100644
--- a/keep/parser/parser.py
+++ b/keep/parser/parser.py
@@ -60,6 +60,7 @@ def parse(
         providers_file: str = None,
         actions_file: str = None,
         workflow_db_id: str = None,
+        workflow_revision: int = None,
     ) -> typing.List[Workflow]:
         """_summary_
 
@@ -82,6 +83,7 @@ def parse(
                     tenant_id,
                     workflow,
                     providers_file,
+                    workflow_revision,
                     workflow_providers,
                     actions_file,
                     workflow_actions,
@@ -98,6 +100,7 @@ def parse(
                 tenant_id,
                 raw_workflow,
                 providers_file,
+                workflow_revision,
                 workflow_providers,
                 actions_file,
                 workflow_actions,
@@ -109,6 +112,7 @@ def parse(
                 tenant_id,
                 parsed_workflow_yaml,
                 providers_file,
+                workflow_revision,
                 workflow_providers,
                 actions_file,
                 workflow_actions,
@@ -139,6 +143,7 @@ def _parse_workflow(
         tenant_id,
         workflow: dict,
         providers_file: str,
+        workflow_revision: int = None,
         workflow_providers: dict = None,
         actions_file: str = None,
         workflow_actions: dict = None,
@@ -188,6 +193,7 @@ def _parse_workflow(
 
         workflow_class = Workflow(
             workflow_id=workflow_id,
+            workflow_revision=workflow_revision,
             workflow_name=workflow_name,
             workflow_description=workflow_description,
             workflow_disabled=workflow_disabled,
diff --git a/keep/workflowmanager/workflow.py b/keep/workflowmanager/workflow.py
index 08e83b87a1..8d6c89ae04 100644
--- a/keep/workflowmanager/workflow.py
+++ b/keep/workflowmanager/workflow.py
@@ -23,6 +23,7 @@ def __init__(
         self,
         context_manager: ContextManager,
         workflow_id: str,
+        workflow_revision: int,
         workflow_name: str,
         workflow_owners: typing.List[str],
         workflow_tags: typing.List[str],
@@ -41,6 +42,7 @@ def __init__(
         workflow_permissions: typing.List[str] = [],
     ):
         self.workflow_id = workflow_id
+        self.workflow_revision = workflow_revision
         self.workflow_name = workflow_name
         self.workflow_owners = workflow_owners
         self.workflow_tags = workflow_tags
diff --git a/keep/workflowmanager/workflowscheduler.py b/keep/workflowmanager/workflowscheduler.py
index 90684128a8..3f283efd15 100644
--- a/keep/workflowmanager/workflowscheduler.py
+++ b/keep/workflowmanager/workflowscheduler.py
@@ -280,6 +280,7 @@ def _run_workflow(
     def handle_manual_event_workflow(
         self,
         workflow_id,
+        workflow_revision,
         tenant_id,
         triggered_by_user,
         event: AlertDto | IncidentDto,
@@ -303,6 +304,7 @@ def handle_manual_event_workflow(
 
             workflow_execution_id = create_workflow_execution(
                 workflow_id=workflow_id,
+                workflow_revision=workflow_revision,
                 tenant_id=tenant_id,
                 triggered_by=f"manually by {triggered_by_user}",
                 execution_number=unique_execution_number,
@@ -503,6 +505,7 @@ def _handle_event_workflows(self):
                         )
                     workflow_execution_id = create_workflow_execution(
                         workflow_id=workflow_id,
+                        workflow_revision=workflow.revision,
                         tenant_id=tenant_id,
                         triggered_by=triggered_by,
                         execution_number=workflow_execution_number,
diff --git a/keep/workflowmanager/workflowstore.py b/keep/workflowmanager/workflowstore.py
index 8f4b2c5839..dd8501b42b 100644
--- a/keep/workflowmanager/workflowstore.py
+++ b/keep/workflowmanager/workflowstore.py
@@ -16,7 +16,6 @@
     get_all_provisioned_workflows,
     get_all_workflows,
     get_all_workflows_yamls,
-    get_raw_workflow,
     get_workflow,
     get_workflow_execution,
 )
@@ -47,7 +46,7 @@ def create_workflow(self, tenant_id: str, created_by, workflow: dict):
         else:
             workflow_name = workflow.get("name")
 
-        workflow = add_or_update_workflow(
+        workflow_db = add_or_update_workflow(
             id=str(uuid.uuid4()),
             name=workflow_name,
             tenant_id=tenant_id,
@@ -57,8 +56,10 @@ def create_workflow(self, tenant_id: str, created_by, workflow: dict):
             is_disabled=Parser.parse_disabled(workflow),
             workflow_raw=cyaml.dump(workflow, width=99999),
         )
-        self.logger.info(f"Workflow {workflow_id} created successfully")
-        return workflow
+        self.logger.info(
+            f"Workflow {workflow_db.id}, {workflow_db.revision} created successfully"
+        )
+        return workflow_db
 
     def delete_workflow(self, tenant_id, workflow_id):
         self.logger.info(f"Deleting workflow {workflow_id}")
@@ -98,21 +99,29 @@ def _parse_workflow_to_dict(self, workflow_path: str) -> dict:
                 return self._read_workflow_from_stream(file)
 
     def get_raw_workflow(self, tenant_id: str, workflow_id: str) -> str:
-        raw_workflow = get_raw_workflow(tenant_id, workflow_id)
-        workflow_yaml = cyaml.safe_load(raw_workflow)
+        workflow = get_workflow(tenant_id, workflow_id)
+        if not workflow:
+            raise HTTPException(
+                status_code=404,
+                detail=f"Workflow {workflow_id} not found",
+            )
+        workflow_yaml = cyaml.safe_load(workflow.workflow_raw)
         valid_workflow_yaml = {"workflow": workflow_yaml}
         return cyaml.dump(valid_workflow_yaml, width=99999)
 
     def get_workflow(self, tenant_id: str, workflow_id: str) -> Workflow:
-        workflow = get_raw_workflow(tenant_id, workflow_id)
+        workflow = get_workflow(tenant_id, workflow_id)
         if not workflow:
             raise HTTPException(
                 status_code=404,
                 detail=f"Workflow {workflow_id} not found",
             )
-        workflow_yaml = cyaml.safe_load(workflow)
+        workflow_yaml = cyaml.safe_load(workflow.workflow_raw)
         workflow = self.parser.parse(
-            tenant_id, workflow_yaml, workflow_db_id=workflow_id
+            tenant_id,
+            workflow_yaml,
+            workflow_db_id=workflow_id,
+            workflow_revision=workflow.revision,
         )
         if len(workflow) > 1:
             raise HTTPException(
@@ -127,9 +136,9 @@ def get_workflow(self, tenant_id: str, workflow_id: str) -> Workflow:
                 detail=f"Workflow {workflow_id} not found",
             )
 
-    def get_workflow_from_dict(self, tenant_id: str, workflow: dict) -> Workflow:
-        logging.info("Parsing workflow from dict", extra={"workflow": workflow})
-        workflow = self.parser.parse(tenant_id, workflow)
+    def get_workflow_from_dict(self, tenant_id: str, workflow_dict: dict) -> Workflow:
+        logging.info("Parsing workflow from dict", extra={"workflow": workflow_dict})
+        workflow = self.parser.parse(tenant_id, workflow_dict)
         if workflow:
             return workflow[0]
         else:
@@ -139,9 +148,7 @@ def get_workflow_from_dict(self, tenant_id: str, workflow: dict) -> Workflow:
             )
 
     def get_all_workflows(self, tenant_id: str) -> list[WorkflowModel]:
-        # list all tenant's workflows
-        workflows = get_all_workflows(tenant_id)
-        return workflows
+        return list(get_all_workflows(tenant_id))
 
     def get_all_workflows_with_last_execution(
         self,
@@ -167,8 +174,7 @@ def get_all_workflows_with_last_execution(
 
     def get_all_workflows_yamls(self, tenant_id: str) -> list[str]:
         # list all tenant's workflows yamls (Workflow.workflow_raw)
-        workflow_yamls = get_all_workflows_yamls(tenant_id)
-        return workflow_yamls
+        return list(get_all_workflows_yamls(tenant_id))
 
     def get_workflows_from_path(
         self,
@@ -539,7 +545,10 @@ def group_last_workflow_executions(self, workflows: list[dict]) -> list[dict]:
         return results
 
     def get_workflow_meta_data(
-        self, tenant_id: str, workflow: WorkflowModel, installed_providers_by_type: dict
+        self,
+        tenant_id: str,
+        workflow: WorkflowModel | None,
+        installed_providers_by_type: dict,
     ):
         providers_dto = []
         triggers = []
diff --git a/tests/test_workflowmanager.py b/tests/test_workflowmanager.py
index 137f3564f3..f4ddd7aac8 100644
--- a/tests/test_workflowmanager.py
+++ b/tests/test_workflowmanager.py
@@ -92,7 +92,7 @@ def test_get_workflow_results():
 def test_handle_manual_event_workflow():
     mock_workflow = Mock(spec=Workflow)
     mock_workflow.workflow_id = "workflow1"
-
+    mock_workflow.workflow_revision = 1
     mock_workflow_manager = Mock()
 
     mock_logger = Mock()
@@ -119,6 +119,7 @@ def test_handle_manual_event_workflow():
 
         workflow_execution_id = workflow_scheduler.handle_manual_event_workflow(
             workflow_id=mock_workflow.workflow_id,
+            workflow_revision=mock_workflow.workflow_revision,
             tenant_id=tenant_id,
             triggered_by_user=triggered_by_user,
             event=event,
@@ -137,6 +138,7 @@ def test_handle_manual_event_workflow():
 def test_handle_manual_event_workflow_test_run():
     mock_workflow = Mock(spec=Workflow)
     mock_workflow.workflow_id = "workflow1"
+    mock_workflow.workflow_revision = 1
 
     mock_workflow_manager = Mock()
 
@@ -164,6 +166,7 @@ def test_handle_manual_event_workflow_test_run():
 
         workflow_execution_id = workflow_scheduler.handle_manual_event_workflow(
             workflow_id=mock_workflow.workflow_id,
+            workflow_revision=mock_workflow.workflow_revision,
             workflow=mock_workflow,
             tenant_id=tenant_id,
             triggered_by_user=triggered_by_user,


Code review comments:
diff_hunk: @@ -0,0 +1,179 @@
+import {
+  ClockIcon,
+  CursorArrowRaysIcon,
+  QuestionMarkCircleIcon,
+} from "@heroicons/react/24/outline";
+import Image from "next/image";
+
+const KeepIncidentIcon = () => (
+  <Image
+    src="/keep.png"
+    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
+    width={16}
+    height={16}
+    alt="Keep Incident"
+  />
+);
+const KeepAlertIcon = () => (
+  <Image
+    src="/keep.png"
+    className="tremor-Badge-icon shrink-0 -ml-1 mr-1.5"
+    width={16}
+    height={16}
+    alt="Keep Alert"
+  />
+);
+
+export function getTriggerIcon(triggered_by: string) {
+  switch (triggered_by) {
+    case "manual":
+      return CursorArrowRaysIcon;
+    case "interval":
+      return ClockIcon;
+    case "alert":
+      return KeepAlertIcon;
+    case "incident":
+      return KeepIncidentIcon;
+    default:
+      return QuestionMarkCircleIcon;
+  }
+}
+
+export function getTriggerIconV2(type: string, details: string) {
+  switch (type) {
+    case "manual":
+      return CursorArrowRaysIcon;
+    case "interval":
+      return ClockIcon;
+    case "alert":
+      return KeepAlertIcon;
+    case "incident":
+      return KeepIncidentIcon;
+    default:
+      return QuestionMarkCircleIcon;
+  }
path: keep-ui/entities/workflows/lib/ui-utils.tsx
text: @user1:
style: getTriggerIconV2 accepts a details parameter but doesn't use it. Consider removing the unused parameter or implementing its usage.

```suggestion
export function getTriggerIconV2(type: string) {
  switch (type) {
    case "manual":
      return CursorArrowRaysIcon;
    case "interval":
      return ClockIcon;
    case "alert":
      return KeepAlertIcon;
    case "incident":
      return KeepIncidentIcon;
    default:
      return QuestionMarkCircleIcon;
  }
```

diff_hunk: @@ -298,18 +326,32 @@ export function WorkflowExecutionResultsInternal({
     />
   );
 
+  console.log("executionData", executionData);
path: keep-ui/features/workflow-execution-results/ui/WorkflowExecutionResults.tsx
text: @user1:
style: Remove debug console.log statement before merging

```suggestion

```

diff_hunk: @@ -0,0 +1,26 @@
+from alembic import op
+import sqlalchemy as sa
+
+
+def drop_foreign_key_constraint(table_name, column_name, referred_table):
+    # First check if the column is nullable (for those who haven't migrated yet)
path: keep/api/models/db/migrations/utils.py
text: @user1:
style: The comment about checking if the column is nullable doesn't match the actual implementation, which is finding the foreign key constraint name.

```suggestion
    # Find and drop the foreign key constraint if it exists
```

diff_hunk: @@ -0,0 +1,108 @@
+"""Workflow Versions
+
+Revision ID: 885ff6b12fed
+Revises: 59991b568c7d
+Create Date: 2025-04-15 15:30:48.099088
+
+"""
+
+import sqlalchemy as sa
+import sqlmodel
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "885ff6b12fed"
+down_revision = "59991b568c7d"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+
+    connection = op.get_bind()
+    dialect = connection.dialect.name
+    op.create_table(
+        "workflowversion",
+        sa.Column("workflow_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("revision", sa.Integer(), nullable=False),
+        sa.Column("workflow_raw", sa.TEXT(), nullable=True),
+        sa.Column("updated_by", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("updated_at", sa.DateTime(), nullable=False),
+        sa.Column("is_valid", sa.Boolean(), nullable=False),
+        sa.Column("is_current", sa.Boolean(), nullable=False),
+        sa.Column("comment", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["workflow_id"],
+            ["workflow.id"],
+        ),
+        sa.PrimaryKeyConstraint("workflow_id", "revision"),
+        sa.UniqueConstraint(
+            "workflow_id", "is_current", name="uq_workflow_current_version"
+        ),
+    )
+    with op.batch_alter_table("workflowexecution", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column("workflow_revision", sa.Integer(), nullable=False)
+        )
+        batch_op.drop_index("idx_workflowexecution_tenant_workflow_id_timestamp")
+        batch_op.create_index(
+            "idx_workflowexecution_tenant_workflow_id_timestamp",
+            ["tenant_id", "workflow_id", "workflow_revision", "started"],
+            unique=False,
+        )
+        batch_op.drop_index(
+            "idx_workflowexecution_workflow_tenant_started_status",
+            mysql_length={"status": 255},
+        )
+        batch_op.create_index(
+            "idx_workflowexecution_workflow_tenant_started_status",
+            [
+                "workflow_id",
+                "workflow_revision",
+                "tenant_id",
+                "started",
+                sa.text("status(255)") if dialect == "mysql" else "status",
+            ],
+            unique=False,
+        )
+        batch_op.create_index(
+            "idx_workflowexecution_workflow_revision",
+            ["workflow_id", "workflow_revision"],
+            unique=False,
+        )
+        batch_op.drop_constraint("workflowexecution_ibfk_2", type_="foreignkey")
+        batch_op.create_foreign_key(None, "workflow", ["workflow_id"], ["id"])
path: keep/api/models/db/migrations/versions/2025-04-15-15-30_885ff6b12fed.py
text: @user1:
logic: Foreign key is recreated without the ondelete='SET DEFAULT' that exists in the downgrade function

diff_hunk: @@ -0,0 +1,26 @@
+from alembic import op
+import sqlalchemy as sa
+
+
+def drop_foreign_key_constraint(table_name, column_name, referred_table):
+    # First check if the column is nullable (for those who haven't migrated yet)
+    connection = op.get_bind()
+    inspector = sa.inspect(connection)
+
+    # Find the actual foreign key constraint name for workflow_id
path: keep/api/models/db/migrations/utils.py
text: @user1:
style: The comment specifically mentions 'workflow_id' but the function is designed to be generic for any column name.

```suggestion
    # Find the actual foreign key constraint name for the column
```

diff_hunk: @@ -28,6 +28,27 @@ class Config:
         orm_mode = True
 
 
+class WorkflowVersion(SQLModel, table=True):
+    __table_args__ = (
+        PrimaryKeyConstraint("workflow_id", "revision"),
+        # Add unique constraint to ensure only one current version per workflow
+        UniqueConstraint(
+            "workflow_id", "is_current", name="uq_workflow_current_version"
+        ),
+    )
+
+    workflow_id: str = Field(primary_key=True, foreign_key="workflow.id")
+    revision: int = Field(primary_key=True)
+    workflow_raw: str = Field(sa_column=Column(TEXT))
+    updated_by: str
+    updated_at: datetime = Field(default_factory=datetime.utcnow)
+    is_valid: bool = Field(default=False)
+    is_current: bool = Field(default=False)
+    comment: Optional[str] = None
+
+    workflow: "Workflow" = Relationship(back_populates="versions")
path: keep/api/models/db/workflow.py
text: @user1:
logic: Missing back_populates relationship from Workflow to WorkflowVersion. Should add `versions: List["WorkflowVersion"] = Relationship(back_populates="workflow")` to the Workflow class.

```suggestion
    workflow: "Workflow" = Relationship(back_populates="versions")

Workflow.versions: List["WorkflowVersion"] = Relationship(back_populates="workflow")
```

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
path: keep/api/core/db.py
text: @user1:
style: Redundant assignment - `name = name` doesn't do anything. Consider removing this condition or simplifying to `name = name if name != "" else existing_workflow.name`

```suggestion
    # In case the workflow name changed to empty string, keep the old name
    name = name if name != "" else existing_workflow.name
```

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
path: keep/api/core/db.py
text: @user1:
a bit confusing name

@author:
@user1 agreed, i went with renaming it to `update_workflow_with_values`. is it better?

diff_hunk: @@ -82,6 +118,13 @@ class WorkflowExecution(SQLModel, table=True):
     execution_time: Optional[int]
     results: dict = Field(sa_column=Column(JSON), default={})
 
+    version: "WorkflowVersion" = Relationship(
+        back_populates="workflow_execution",
+        sa_relationship_kwargs={
+            "foreign_keys": "[WorkflowExecution.workflow_id, WorkflowExecution.workflow_revision]"
+        },
+    )
path: keep/api/models/db/workflow.py
text: @user1:
logic: The relationship is defined but WorkflowVersion doesn't have a corresponding back_populates field for workflow_execution.

```suggestion
    version: "WorkflowVersion" = Relationship(
        back_populates="workflow_execution",
        sa_relationship_kwargs={
            "foreign_keys": "[WorkflowExecution.workflow_id, WorkflowExecution.workflow_revision]"
        },
    )

WorkflowVersion.workflow_execution: List["WorkflowExecution"] = Relationship(back_populates="version")
```

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
path: keep/api/core/db.py
text: @user1:
```suggestion
    name = name or existing_workflow.name
```

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
+    with Session(engine, expire_on_commit=False) as session:
path: keep/api/core/db.py
text: @user1:
it's another session you open right after using update_workflow_by_id, seems kinda redundant

@author:
updated with `with existed_or_new_session(session) as session:`

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
+    with Session(engine, expire_on_commit=False) as session:
+        # TODO: ensure that id is unique to workflow_id, like if rolled back and now updating from this version it should be bigger than latest version
+        next_revision = existing_workflow.revision + 1
+        updated_by_str = updated_by or existing_workflow.updated_by or "unknown"
+        # update version
+        version = WorkflowVersion(
+            workflow_id=existing_workflow.id,
+            revision=next_revision,
+            workflow_raw=workflow_raw,
+            updated_by=updated_by_str,
+            comment=f"Updated by {updated_by_str}",
+            # TODO: check if valid
+            is_valid=True,
+            is_current=True,
+        )
+        session.add(version)
+
+        existing_version = session.exec(
+            select(WorkflowVersion)
+            .where(WorkflowVersion.workflow_id == existing_workflow.id)
+            .where(WorkflowVersion.revision == existing_workflow.revision)
+        ).first()
+        if existing_version:
+            existing_version.is_current = False
+            session.add(existing_version)
path: keep/api/core/db.py
text: @user1:
why do you need to query it again? If you were using the same session, it was just

```suggestion
        existing_version.is_current = False
```

@author:
@user1 i'm creating a new version, and then updating the old one, which I query for. existing_version var doesn't exist yet where you suggest to use it. am I missing something?

@user1:
the function `update_workflow_by_workflow` gets the parameter `existing_version`, unless I miss something

@author:
@user1 no, only existing_workflow, which is different

@user1:
meh, gotcha.. sorry for the confusion

diff_hunk: @@ -430,6 +435,94 @@ def get_workflows_that_should_run():
         return workflows_to_run
 
 
+def update_workflow_by_id(
+    id,
+    name,
+    tenant_id,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    with Session(engine, expire_on_commit=False):
+        existing_workflow = get_workflow(tenant_id, id)
+        if not existing_workflow:
+            raise ValueError("Workflow not found")
+        return update_workflow_by_workflow(
+            existing_workflow,
+            name=name,
+            description=description,
+            interval=interval,
+            workflow_raw=workflow_raw,
+            is_disabled=is_disabled,
+            provisioned=provisioned,
+            provisioned_file=provisioned_file,
+            updated_by=updated_by,
+        )
+
+
+def update_workflow_by_workflow(
+    existing_workflow: Workflow,
+    name,
+    description,
+    interval,
+    workflow_raw,
+    is_disabled,
+    provisioned=False,
+    provisioned_file=None,
+    updated_by=None,
+):
+    # In case the workflow name changed to empty string, keep the old name
+    if name != "":
+        name = name
+    else:
+        name = existing_workflow.name
+    with Session(engine, expire_on_commit=False) as session:
+        # TODO: ensure that id is unique to workflow_id, like if rolled back and now updating from this version it should be bigger than latest version
+        next_revision = existing_workflow.revision + 1
+        updated_by_str = updated_by or existing_workflow.updated_by or "unknown"
path: keep/api/core/db.py
text: @user1:
i feel like unknown should be a wtf exception - how can that happen?