Repository: keephq/keep
Pull request: feat: Improve user interaction with workflow templates
<!-- 
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 #3814

## 📑 Description
<!-- Add a brief description of the pr -->
https://github.com/user-attachments/assets/82516533-d5b3-4982-8969-ff08bad1f294


https://github.com/user-attachments/assets/0dcfa2a1-b70d-435a-8d40-27e8f5626590





<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->

## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Problem statement:
[➕ Feature]: Improve user interaction with workflow templates
**Is your feature request related to a problem? Please describe.**
Currently we display random templates.

**Describe the solution you'd like**
1. When no workflows created - display templates
2. When at least one workflow is created - no templates
3. When the user tries to create a workflow - display screen that suggests existing templates or manually build a workflow (regardless of whether workflows have been created before or not)
4. Workflow templates should support search by name and description

Hints from issue discussion:

Commit message: remove unused mock for WorkflowTemplates in existing workflows state test

Patch to review:
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 d7e5b97e59..a29f0e64c5 100644
--- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx
+++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx
@@ -8,7 +8,7 @@ import WorkflowGraph from "../workflow-graph";
 import { WorkflowExecutionsTable } from "./workflow-executions-table";
 import { WorkflowOverviewSkeleton } from "./workflow-overview-skeleton";
 import { WorkflowProviders } from "./workflow-providers";
-import { WorkflowSteps } from "../workflows-templates";
+import { WorkflowSteps } from "../workflows-steps";
 import { parseWorkflowYamlStringToJSON } from "@/entities/workflows/lib/yaml-utils";
 interface Pagination {
   limit: number;
diff --git a/keep-ui/app/(keep)/workflows/__tests__/workflows.client.test.tsx b/keep-ui/app/(keep)/workflows/__tests__/existing-workflows-state.test.tsx
similarity index 88%
rename from keep-ui/app/(keep)/workflows/__tests__/workflows.client.test.tsx
rename to keep-ui/app/(keep)/workflows/__tests__/existing-workflows-state.test.tsx
index 3f7714d445..42ee51faa0 100644
--- a/keep-ui/app/(keep)/workflows/__tests__/workflows.client.test.tsx
+++ b/keep-ui/app/(keep)/workflows/__tests__/existing-workflows-state.test.tsx
@@ -1,5 +1,5 @@
 import { act, fireEvent, getByText, render } from "@testing-library/react";
-import { WorkflowsPage } from "../workflows.client";
+import { ExistingWorkflowsState } from "../existing-workflows-state";
 import { useWorkflowsV2 } from "@/entities/workflows/model/useWorkflowsV2";
 import { useWorkflowActions } from "@/entities/workflows/model/useWorkflowActions";
 import { mockWorkflow } from "@/entities/workflows/model/__mocks__/mock-workflow";
@@ -28,10 +28,6 @@ jest.mock("@/features/filter/facet-panel-server-side", () => ({
   FacetsPanelServerSide: () => <div data-testid="facets-panel" />,
 }));
 
-jest.mock("@/app/(keep)/workflows/workflows-templates", () => ({
-  WorkflowTemplates: () => <div data-testid="workflow-templates" />,
-}));
-
 describe("WorkflowsPage", () => {
   afterEach(() => {
     jest.clearAllMocks();
@@ -45,7 +41,7 @@ describe("WorkflowsPage", () => {
       error: null,
     });
 
-    const { getByTestId } = render(<WorkflowsPage />);
+    const { getByTestId } = render(<ExistingWorkflowsState />);
 
     expect(getByTestId("workflow-list")).toBeInTheDocument();
   });
@@ -58,7 +54,7 @@ describe("WorkflowsPage", () => {
       error: null,
     });
 
-    const { getByTestId } = render(<WorkflowsPage />);
+    const { getByTestId } = render(<ExistingWorkflowsState />);
 
     await act(async () => {
       const workflowList = getByTestId("workflow-list");
diff --git a/keep-ui/app/(keep)/workflows/create-workflow-modal/create-workflow-modal.tsx b/keep-ui/app/(keep)/workflows/create-workflow-modal/create-workflow-modal.tsx
new file mode 100644
index 0000000000..3ca46224ee
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/create-workflow-modal/create-workflow-modal.tsx
@@ -0,0 +1,46 @@
+import Modal from "@/components/ui/Modal";
+import "react-loading-skeleton/dist/skeleton.css";
+import { WorkflowTemplates } from "./workflow-templates";
+import { useRouter } from "next/navigation";
+import { Button } from "@tremor/react";
+
+interface CreateWorkflowModalProps {
+  onClose: () => void;
+}
+
+export const CreateWorkflowModal: React.FC<CreateWorkflowModalProps> = ({
+  onClose,
+}) => {
+  const router = useRouter();
+
+  return (
+    <Modal
+      isOpen={true}
+      onClose={onClose}
+      className="min-w-[80vw] min-h-[90vh]"
+      title="Create workflow"
+    >
+      <div className="flex flex-col gap-3 mb-3 h-full w-full">
+        <div className="text-lg">
+          <p>
+            Choose a workflow template to start building the automation for your
+            alerts and incidents.
+          </p>
+          <p className="pt-2 pb-1">
+            Or skip this, and{" "}
+            <Button
+              className="ml-2"
+              color="orange"
+              size="xs"
+              variant="primary"
+              onClick={() => router.push("/workflows/builder")}
+            >
+              Start from scratch
+            </Button>
+          </p>
+        </div>
+      </div>
+      <WorkflowTemplates></WorkflowTemplates>
+    </Modal>
+  );
+};
diff --git a/keep-ui/app/(keep)/workflows/create-workflow-modal/index.ts b/keep-ui/app/(keep)/workflows/create-workflow-modal/index.ts
new file mode 100644
index 0000000000..4326537768
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/create-workflow-modal/index.ts
@@ -0,0 +1,2 @@
+export { CreateWorkflowModal } from "./create-workflow-modal";
+export { WorkflowTemplates } from "./workflow-templates";
diff --git a/keep-ui/app/(keep)/workflows/create-workflow-modal/use-query-workflow-template.ts b/keep-ui/app/(keep)/workflows/create-workflow-modal/use-query-workflow-template.ts
new file mode 100644
index 0000000000..79e9cb04c5
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/create-workflow-modal/use-query-workflow-template.ts
@@ -0,0 +1,35 @@
+import { WorkflowTemplate } from "@/shared/api/workflows";
+import { useApi } from "@/shared/lib/hooks/useApi";
+import useSWR, { SWRConfiguration } from "swr";
+
+export interface WorkflowTemplatesQuery {
+  cel: string;
+  limit: number;
+  offset: number;
+}
+
+export function useQueryWorkflowTemplate(
+  query: WorkflowTemplatesQuery,
+  options?: SWRConfiguration<any>
+) {
+  const api = useApi();
+  const requestUrl = `/workflows/templates/query`;
+  const { data, error, isLoading, mutate } = useSWR<any>(
+    api.isReady() && query
+      ? `/workflows/templates/query` + JSON.stringify(query)
+      : null,
+    () => api.post(requestUrl, query),
+    {
+      revalidateOnFocus: false,
+      ...options,
+    }
+  );
+
+  return {
+    data: data?.results as WorkflowTemplate[],
+    totalCount: data?.count,
+    error,
+    isLoading,
+    mutate,
+  };
+}
diff --git a/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-template-card.tsx b/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-template-card.tsx
new file mode 100644
index 0000000000..2c2780035a
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-template-card.tsx
@@ -0,0 +1,56 @@
+import { WorkflowSteps } from "../workflows-steps";
+import { WorkflowTemplate } from "@/shared/api/workflows";
+import { Button, Card } from "@tremor/react";
+import { useRouter } from "next/navigation";
+import Skeleton from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
+
+export const WorkflowTemplateCard: React.FC<{ template: WorkflowTemplate }> = ({
+  template,
+}) => {
+  const router = useRouter();
+  const handlePreview = (template: WorkflowTemplate) => {
+    localStorage.setItem("preview_workflow", JSON.stringify(template));
+    router.push(`/workflows/preview/${template.workflow_raw_id}`);
+  };
+  const getNameFromId = (id: string) => {
+    if (!id) {
+      return "";
+    }
+    return id.split("-").join(" ");
+  };
+  return (
+    <Card
+      className="p-4 flex flex-col justify-between w-full border-2 border-transparent hover:border-orange-400 gap-2"
+      onClick={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        handlePreview(template);
+      }}
+    >
+      <div className="min-h-36">
+        {template && <WorkflowSteps workflow={template.workflow} />}
+        {!template && <Skeleton className="h-4 w-16 mb-2" />}
+        <h3 className="text-lg sm:text-xl font-semibold line-clamp-2">
+          {template && getNameFromId(template.workflow.id)}
+          {!template && <Skeleton className="h-4 w-28 mb-2" />}
+        </h3>
+        <p className="mt-2 text-sm sm:text-base line-clamp-3">
+          {template && template.workflow.description}
+          {!template && <Skeleton className="h-16 w-full mb-2" />}
+        </p>
+      </div>
+      <div>
+        {template && (
+          <Button
+            variant="secondary"
+            //   disabled={!!(loadingId && loadingId !== workflow.id)}
+            //   loading={loadingId === workflow.id}
+          >
+            Preview
+          </Button>
+        )}
+      </div>
+    </Card>
+  );
+};
diff --git a/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-templates.tsx b/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-templates.tsx
new file mode 100644
index 0000000000..9ed8936512
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-templates.tsx
@@ -0,0 +1,123 @@
+import "react-loading-skeleton/dist/skeleton.css";
+import { useQueryWorkflowTemplate } from "./use-query-workflow-template";
+import { useCallback, useMemo, useState } from "react";
+import { SearchInput } from "@/features/filter";
+import { Pagination, PaginationState } from "@/features/filter/pagination";
+import { WorkflowTemplateCard } from "./workflow-template-card";
+import { useRouter } from "next/navigation";
+import { ErrorComponent } from "@/shared/ui";
+import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
+import { Button } from "@tremor/react";
+
+interface WorkflowTemplatesProps {}
+
+export const WorkflowTemplates: React.FC<WorkflowTemplatesProps> = () => {
+  const router = useRouter();
+
+  const [searchValue, setSearchValue] = useState("");
+  const [paginationState, setPaginationState] = useState<PaginationState>({
+    offset: 0,
+    limit: 12,
+  });
+
+  const query = useMemo(() => {
+    const cel = searchValue
+      ? `name.contains("${searchValue}") || description.contains("${searchValue}")`
+      : "";
+    return {
+      cel,
+      limit: paginationState.limit,
+      offset: paginationState.offset,
+    };
+  }, [searchValue, paginationState]);
+
+  const {
+    data: mockWorkflows,
+    totalCount,
+    error: mockError,
+    isLoading: mockLoading,
+    mutate: refresh,
+  } = useQueryWorkflowTemplate(query, {
+    revalidateOnFocus: false,
+  });
+
+  const cartsToRender = useMemo(() => {
+    if (mockLoading || !mockWorkflows) {
+      return new Array(paginationState.limit).fill(undefined);
+    }
+
+    return mockWorkflows;
+  }, [mockWorkflows, mockLoading, paginationState]);
+
+  const setPaginationStateCallback = useCallback(
+    (pageIndex: number, limit: number, offset: number) => {
+      setPaginationState({ limit, offset });
+    },
+    [setPaginationState]
+  );
+
+  function renderBody() {
+    if (mockError) {
+      return <ErrorComponent error={mockError} reset={() => refresh()} />;
+    }
+
+    if (cartsToRender.length === 0) {
+      return (
+        <div className="flex items-center h-[640px]">
+          <div className="flex flex-col justify-center items-center w-full">
+            <div className="flex flex-col items-center justify-center max-w-md">
+              <MagnifyingGlassIcon
+                className="mx-auto size-8 text-tremor-content-strong/80"
+                aria-hidden={true}
+              />
+              <p className="mt-2 text-xl font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
+                No workflows to display matching your search
+              </p>
+            </div>
+            <Button
+              className="mt-4"
+              color="orange"
+              variant="secondary"
+              onClick={() => setSearchValue("")}
+            >
+              Clear search
+            </Button>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <>
+        <div className="min-h-[640px]">
+          <div className="flex-1  grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full gap-4">
+            {cartsToRender.map((template, index: number) => (
+              <WorkflowTemplateCard key={index} template={template} />
+            ))}
+          </div>
+        </div>
+        <div className={mockLoading ? "hidden" : ""}>
+          <Pagination
+            totalCount={totalCount ?? 0}
+            isRefreshAllowed={false}
+            isRefreshing={false}
+            pageSizeOptions={[12]}
+            onRefresh={() => {}}
+            onStateChange={setPaginationStateCallback}
+          />
+        </div>
+      </>
+    );
+  }
+
+  return (
+    <div className="flex flex-col gap-3 h-full w-full">
+      <SearchInput
+        placeholder="Search workflows"
+        value={searchValue as string}
+        onValueChange={setSearchValue}
+      />
+      {renderBody()}
+    </div>
+  );
+};
diff --git a/keep-ui/app/(keep)/workflows/workflows.client.tsx b/keep-ui/app/(keep)/workflows/existing-workflows-state.tsx
similarity index 62%
rename from keep-ui/app/(keep)/workflows/workflows.client.tsx
rename to keep-ui/app/(keep)/workflows/existing-workflows-state.tsx
index f6b986437c..177b03728b 100644
--- a/keep-ui/app/(keep)/workflows/workflows.client.tsx
+++ b/keep-ui/app/(keep)/workflows/existing-workflows-state.tsx
@@ -1,31 +1,22 @@
 "use client";
 
-import {
-  ChangeEvent,
-  useCallback,
-  useEffect,
-  useMemo,
-  useRef,
-  useState,
-} from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { Button } from "@tremor/react";
 import { ArrowUpOnSquareStackIcon } from "@heroicons/react/24/outline";
-import { EmptyStateCard, KeepLoader, PageTitle } from "@/shared/ui";
+import {
+  EmptyStateCard,
+  ErrorComponent,
+  KeepLoader,
+  PageTitle,
+} from "@/shared/ui";
 import WorkflowsEmptyState from "./noworkflows";
 import WorkflowTile from "./workflow-tile";
-import { ArrowRightIcon } from "@radix-ui/react-icons";
-import { useRouter } from "next/navigation";
-import Modal from "@/components/ui/Modal";
-import { WorkflowTemplates } from "./workflows-templates";
-import { Input, ErrorComponent } from "@/shared/ui";
-import { Textarea } from "@/components/ui";
 import {
   DEFAULT_WORKFLOWS_PAGINATION,
   DEFAULT_WORKFLOWS_QUERY,
   useWorkflowsV2,
   WorkflowsQuery,
 } from "@/entities/workflows/model/useWorkflowsV2";
-import { useWorkflowActions } from "@/entities/workflows/model/useWorkflowActions";
 import { PageSubtitle } from "@/shared/ui/PageSubtitle";
 import { PlusIcon } from "@heroicons/react/20/solid";
 import { UserStatefulAvatar } from "@/entities/users/ui";
@@ -44,62 +35,23 @@ import { FacetsPanelServerSide } from "@/features/filter/facet-panel-server-side
 import { InitialFacetsData } from "@/features/filter/api";
 import { v4 as uuidV4 } from "uuid";
 import { PaginationState } from "@/features/filter/pagination";
-
-const EXAMPLE_WORKFLOW_DEFINITIONS = {
-  slack: `
-    workflow:
-      id: slack-demo
-      description: Send a slack message when any alert is triggered or manually
-      triggers:
-        - type: alert
-        - type: manual
-      actions:
-        - name: trigger-slack
-          provider:
-            type: slack
-            config: " {{ providers.slack }} "
-            with:
-              message: "Workflow ran | reason: {{ event.trigger }}"
-    `,
-  sql: `
-    workflow:
-      id: bq-sql-query
-      description: Run SQL on Bigquery and send the results to slack
-      triggers:
-        - type: manual
-      steps:
-        - name: get-sql-data
-          provider:
-            type: bigquery
-            config: "{{ providers.bigquery-prod }}"
-            with:
-              query: "SELECT * FROM some_database LIMIT 1"
-      actions:
-        - name: trigger-slack
-          provider:
-            type: slack
-            config: " {{ providers.slack-prod }} "
-            with:
-              message: "Results from the DB: ({{ steps.get-sql-data.results }})"
-  `,
-};
-
-type ExampleWorkflowKey = keyof typeof EXAMPLE_WORKFLOW_DEFINITIONS;
+import { CreateWorkflowModal } from "./create-workflow-modal/create-workflow-modal";
+import { UploadWorkflowsModal } from "./upload-workflows-modal";
 
 const AssigneeLabel = ({ email }: { email: string }) => {
   const user = useUser(email);
   return user ? user.name : email;
 };
 
-export function WorkflowsPage({
+export function ExistingWorkflowsState({
   initialFacetsData,
 }: {
   initialFacetsData?: InitialFacetsData;
 }) {
-  const router = useRouter();
-  const fileInputRef = useRef<HTMLInputElement | null>(null);
-  const [workflowDefinition, setWorkflowDefinition] = useState("");
-  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isUploadWorkflowsModalOpen, setIsUploadWorkflowsModalOpen] =
+    useState(false);
+  const [isCreateWorkflowModalOpen, setIsCreateWorkflowModalOpen] =
+    useState(false);
   const [clearFiltersToken, setClearFiltersToken] = useState<string | null>(
     null
   );
@@ -157,8 +109,6 @@ export function WorkflowsPage({
 
   const isFirstLoading = isFilteredWorkflowsLoading && !filteredWorkflows;
 
-  const { uploadWorkflowFiles } = useWorkflowActions();
-
   const isTableEmpty = filteredWorkflowsCount === 0;
   const isEmptyState =
     !isFilteredWorkflowsLoading && isTableEmpty && !workflowsQuery?.cel;
@@ -301,57 +251,6 @@ export function WorkflowsPage({
     return <ErrorComponent error={error} reset={() => {}} />;
   }
 
-  const onDrop = async (files: ChangeEvent<HTMLInputElement>) => {
-    if (!files.target.files) {
-      return;
-    }
-
-    const uploadedWorkflowsIds = await uploadWorkflowFiles(files.target.files);
-
-    if (fileInputRef.current) {
-      // Reset the file input to allow for multiple uploads
-      fileInputRef.current.value = "";
-    }
-
-    setIsModalOpen(false);
-    if (uploadedWorkflowsIds.length === 1) {
-      // If there is only one file, redirect to the workflow detail page
-      router.push(`/workflows/${uploadedWorkflowsIds[0]}`);
-    }
-  };
-
-  function handleWorkflowDefinitionString(
-    workflowDefinition: string,
-    name: string = "New workflow"
-  ) {
-    const blob = new Blob([workflowDefinition], {
-      type: "application/x-yaml",
-    });
-    const file = new File([blob], `${name}.yml`, {
-      type: "application/x-yaml",
-    });
-    const event = {
-      target: {
-        files: [file],
-      },
-    };
-    onDrop(event as any);
-  }
-
-  function handleStaticExampleSelect(exampleKey: ExampleWorkflowKey) {
-    switch (exampleKey) {
-      case "slack":
-        handleWorkflowDefinitionString(EXAMPLE_WORKFLOW_DEFINITIONS.slack);
-        break;
-      case "sql":
-        handleWorkflowDefinitionString(EXAMPLE_WORKFLOW_DEFINITIONS.sql);
-        break;
-      default:
-        throw new Error(`Invalid example workflow key: ${exampleKey}`);
-    }
-    setIsModalOpen(false);
-  }
-
   return (
     <>
       <main className="flex flex-col gap-12">
@@ -369,7 +268,7 @@ export function WorkflowsPage({
                 size="md"
                 variant="secondary"
                 onClick={() => {
-                  setIsModalOpen(true);
+                  setIsUploadWorkflowsModalOpen(true);
                 }}
                 icon={ArrowUpOnSquareStackIcon}
                 id="uploadWorkflowButton"
@@ -380,7 +279,7 @@ export function WorkflowsPage({
                 color="orange"
                 size="md"
                 variant="primary"
-                onClick={() => router.push("/workflows/builder")}
+                onClick={() => setIsCreateWorkflowModalOpen(true)}
                 icon={PlusIcon}
               >
                 Create Workflow
@@ -439,101 +338,17 @@ export function WorkflowsPage({
             </div>
           )}
         </div>
-        <div className="flex flex-col gap-4">
-          <WorkflowTemplates />
-        </div>
       </main>
-      <Modal
-        isOpen={isModalOpen}
-        onClose={() => setIsModalOpen(false)}
-        title="Upload Workflow files"
-      >
-        <div className="bg-white rounded max-w-lg max-h-fit	 mx-auto z-20">
-          <div className="space-y-2">
-            <Input
-              ref={fileInputRef}
-              id="workflowFile"
-              name="file"
-              type="file"
-              className="mt-2"
-              accept=".yml, .yaml"
-              multiple
-              onChange={(e) => {
-                onDrop(e);
-                setIsModalOpen(false); // Add this line to close the modal
-              }}
-            />
-            <p className="mt-2 text-xs text-gray-500 dark:text-gray-500">
-              Only .yml and .yaml files are supported.
-            </p>
-          </div>
-          <div className="mt-4">
-            <h3>Or paste the YAML definition:</h3>
-            <Textarea
-              id="workflowDefinition"
-              onChange={(e) => {
-                setWorkflowDefinition(e.target.value);
-              }}
-              name="workflowDefinition"
-              className="mt-2"
-            />
-            <Button
-              className="mt-2"
-              color="orange"
-              size="md"
-              variant="primary"
-              onClick={() => handleWorkflowDefinitionString(workflowDefinition)}
-            >
-              Load
-            </Button>
-          </div>
-          <div className="mt-4 text-sm">
-            <h3>Or just try some from Keep examples:</h3>
-            <Button
-              className="mt-2"
-              color="orange"
-              size="md"
-              variant="secondary"
-              icon={ArrowRightIcon}
-              onClick={() => handleStaticExampleSelect("slack")}
-            >
-              Send a Slack message for every alert or manually
-            </Button>
-
-            <Button
-              className="mt-2"
-              color="orange"
-              size="md"
-              variant="secondary"
-              icon={ArrowRightIcon}
-              onClick={() => handleStaticExampleSelect("sql")}
-            >
-              Run SQL query and send the results as a Slack message
-            </Button>
-
-            <p className="mt-2">
-              More examples at{" "}
-              <a
-                href="https://github.com/keephq/keep/tree/main/examples/workflows"
-                target="_blank"
-              >
-                Keep GitHub repo
-              </a>
-            </p>
-          </div>
-
-          <div className="mt-4">
-            <Button
-              className="mt-2"
-              color="orange"
-              variant="secondary"
-              onClick={() => setIsModalOpen(false)}
-            >
-              Cancel
-            </Button>
-          </div>
-        </div>
-      </Modal>
+      {isUploadWorkflowsModalOpen && (
+        <UploadWorkflowsModal
+          onClose={() => setIsUploadWorkflowsModalOpen(false)}
+        />
+      )}
+      {isCreateWorkflowModalOpen && (
+        <CreateWorkflowModal
+          onClose={() => setIsCreateWorkflowModalOpen(false)}
+        />
+      )}
     </>
   );
 }
diff --git a/keep-ui/app/(keep)/workflows/no-workflows-state.tsx b/keep-ui/app/(keep)/workflows/no-workflows-state.tsx
new file mode 100644
index 0000000000..b9cf5e0d2c
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/no-workflows-state.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { WorkflowTemplates } from "./create-workflow-modal";
+import { InitialFacetsData } from "@/features/filter/api";
+import { useRouter } from "next/navigation";
+import { Button } from "@tremor/react";
+import { useState } from "react";
+import { ArrowUpOnSquareStackIcon } from "@heroicons/react/24/outline";
+import { UploadWorkflowsModal } from "./upload-workflows-modal";
+
+export function NoWorkflowsState({}: {
+  initialFacetsData?: InitialFacetsData;
+}) {
+  const [isUploadWorkflowsModalOpen, setIsUploadWorkflowsModalOpen] =
+    useState(false);
+  const router = useRouter();
+
+  return (
+    <>
+      <p className="text-3xl font-bold mb-3">Create your first workflow</p>
+      <div className="flex flex-col gap-3 mb-3">
+        <div className="text-lg">
+          <p>
+            Choose a workflow template to start building the automation for your
+            alerts and incidents.
+          </p>
+          <div className="flex items-center gap-2">
+            <span>You can also</span>
+            <Button
+              color="orange"
+              size="md"
+              variant="secondary"
+              onClick={() => {
+                setIsUploadWorkflowsModalOpen(true);
+              }}
+              icon={ArrowUpOnSquareStackIcon}
+              id="uploadWorkflowButton"
+            >
+              Upload Workflows
+            </Button>
+            <span>or</span>
+            <Button
+              color="orange"
+              size="xs"
+              variant="primary"
+              onClick={() => router.push("/workflows/builder")}
+            >
+              Start from scratch
+            </Button>
+          </div>
+        </div>
+      </div>
+      <WorkflowTemplates></WorkflowTemplates>
+      {isUploadWorkflowsModalOpen && (
+        <UploadWorkflowsModal
+          onClose={() => setIsUploadWorkflowsModalOpen(false)}
+        />
+      )}
+    </>
+  );
+}
diff --git a/keep-ui/app/(keep)/workflows/page.tsx b/keep-ui/app/(keep)/workflows/page.tsx
index da95c6fec3..09187b7bd9 100644
--- a/keep-ui/app/(keep)/workflows/page.tsx
+++ b/keep-ui/app/(keep)/workflows/page.tsx
@@ -1,5 +1,5 @@
 import React from "react";
-import { WorkflowsPage } from "./workflows.client";
+import { WorkflowsPage } from "./workflows.page";
 import { FacetDto } from "@/features/filter";
 import { createServerApiClient } from "@/shared/api/server";
 import { getInitialFacets } from "@/features/filter/api";
diff --git a/keep-ui/app/(keep)/workflows/upload-workflows-modal.tsx b/keep-ui/app/(keep)/workflows/upload-workflows-modal.tsx
new file mode 100644
index 0000000000..8325195c50
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/upload-workflows-modal.tsx
@@ -0,0 +1,204 @@
+import React from "react";
+import { ChangeEvent, useRef, useState } from "react";
+import { Button } from "@tremor/react";
+import { ArrowRightIcon } from "@radix-ui/react-icons";
+import { useRouter } from "next/navigation";
+import Modal from "@/components/ui/Modal";
+import { Input } from "@/shared/ui";
+import { Textarea } from "@/components/ui";
+import { useWorkflowActions } from "@/entities/workflows/model/useWorkflowActions";
+
+const EXAMPLE_WORKFLOW_DEFINITIONS = {
+  slack: `
+      workflow:
+        id: slack-demo
+        description: Send a slack message when any alert is triggered or manually
+        triggers:
+          - type: alert
+          - type: manual
+        actions:
+          - name: trigger-slack
+            provider:
+              type: slack
+              config: " {{ providers.slack }} "
+              with:
+                message: "Workflow ran | reason: {{ event.trigger }}"
+      `,
+  sql: `
+      workflow:
+        id: bq-sql-query
+        description: Run SQL on Bigquery and send the results to slack
+        triggers:
+          - type: manual
+        steps:
+          - name: get-sql-data
+            provider:
+              type: bigquery
+              config: "{{ providers.bigquery-prod }}"
+              with:
+                query: "SELECT * FROM some_database LIMIT 1"
+        actions:
+          - name: trigger-slack
+            provider:
+              type: slack
+              config: " {{ providers.slack-prod }} "
+              with:
+                message: "Results from the DB: ({{ steps.get-sql-data.results }})"
+    `,
+};
+
+type ExampleWorkflowKey = keyof typeof EXAMPLE_WORKFLOW_DEFINITIONS;
+
+interface UploadWorkflowsModalProps {
+  onClose: () => void;
+}
+
+export const UploadWorkflowsModal: React.FC<UploadWorkflowsModalProps> = ({
+  onClose,
+}) => {
+  const fileInputRef = useRef<HTMLInputElement | null>(null);
+  const [workflowDefinition, setWorkflowDefinition] = useState("");
+  const { uploadWorkflowFiles } = useWorkflowActions();
+  const router = useRouter();
+
+  const onDrop = async (files: ChangeEvent<HTMLInputElement>) => {
+    if (!files.target.files) {
+      return;
+    }
+
+    const uploadedWorkflowsIds = await uploadWorkflowFiles(files.target.files);
+
+    if (fileInputRef.current) {
+      // Reset the file input to allow for multiple uploads
+      fileInputRef.current.value = "";
+    }
+
+    onClose();
+    if (uploadedWorkflowsIds.length === 1) {
+      // If there is only one file, redirect to the workflow detail page
+      router.push(`/workflows/${uploadedWorkflowsIds[0]}`);
+    }
+  };
+
+  function handleWorkflowDefinitionString(
+    workflowDefinition: string,
+    name: string = "New workflow"
+  ) {
+    const blob = new Blob([workflowDefinition], {
+      type: "application/x-yaml",
+    });
+    const file = new File([blob], `${name}.yml`, {
+      type: "application/x-yaml",
+    });
+    const event = {
+      target: {
+        files: [file],
+      },
+    };
+    onDrop(event as any);
+  }
+
+  function handleStaticExampleSelect(exampleKey: ExampleWorkflowKey) {
+    switch (exampleKey) {
+      case "slack":
+        handleWorkflowDefinitionString(EXAMPLE_WORKFLOW_DEFINITIONS.slack);
+        break;
+      case "sql":
+        handleWorkflowDefinitionString(EXAMPLE_WORKFLOW_DEFINITIONS.sql);
+        break;
+      default:
+        throw new Error(`Invalid example workflow key: ${exampleKey}`);
+    }
+    onClose();
+  }
+
+  return (
+    <Modal isOpen={true} onClose={onClose} title="Upload Workflow files">
+      <div className="bg-white rounded max-w-lg max-h-fit	 mx-auto z-20">
+        <div className="space-y-2">
+          <Input
+            ref={fileInputRef}
+            id="workflowFile"
+            name="file"
+            type="file"
+            className="mt-2"
+            accept=".yml, .yaml"
+            multiple
+            onChange={(e) => {
+              onDrop(e);
+              onClose(); // Add this line to close the modal
+            }}
+          />
+          <p className="mt-2 text-xs text-gray-500 dark:text-gray-500">
+            Only .yml and .yaml files are supported.
+          </p>
+        </div>
+        <div className="mt-4">
+          <h3>Or paste the YAML definition:</h3>
+          <Textarea
+            id="workflowDefinition"
+            onChange={(e) => {
+              setWorkflowDefinition(e.target.value);
+            }}
+            name="workflowDefinition"
+            className="mt-2"
+          />
+          <Button
+            className="mt-2"
+            color="orange"
+            size="md"
+            variant="primary"
+            onClick={() => handleWorkflowDefinitionString(workflowDefinition)}
+          >
+            Load
+          </Button>
+        </div>
+        <div className="mt-4 text-sm">
+          <h3>Or just try some from Keep examples:</h3>
+          <Button
+            className="mt-2"
+            color="orange"
+            size="md"
+            variant="secondary"
+            icon={ArrowRightIcon}
+            onClick={() => handleStaticExampleSelect("slack")}
+          >
+            Send a Slack message for every alert or manually
+          </Button>
+
+          <Button
+            className="mt-2"
+            color="orange"
+            size="md"
+            variant="secondary"
+            icon={ArrowRightIcon}
+            onClick={() => handleStaticExampleSelect("sql")}
+          >
+            Run SQL query and send the results as a Slack message
+          </Button>
+
+          <p className="mt-2">
+            More examples at{" "}
+            <a
+              href="https://github.com/keephq/keep/tree/main/examples/workflows"
+              target="_blank"
+            >
+              Keep GitHub repo
+            </a>
+          </p>
+        </div>
+
+        <div className="mt-4">
+          <Button
+            className="mt-2"
+            color="orange"
+            variant="secondary"
+            onClick={() => onClose()}
+          >
+            Cancel
+          </Button>
+        </div>
+      </div>
+    </Modal>
+  );
+};
diff --git a/keep-ui/app/(keep)/workflows/workflows-steps.tsx b/keep-ui/app/(keep)/workflows/workflows-steps.tsx
new file mode 100644
index 0000000000..375105c15a
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/workflows-steps.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { MockStep, MockWorkflow } from "@/shared/api/workflows";
+import { TiArrowRight } from "react-icons/ti";
+import "react-loading-skeleton/dist/skeleton.css";
+import { DynamicImageProviderIcon } from "@/components/ui";
+
+export function WorkflowSteps({ workflow }: { workflow: MockWorkflow }) {
+  const isStepPresent =
+    !!workflow?.steps?.length &&
+    workflow?.steps?.find((step: MockStep) => step?.provider?.type);
+
+  return (
+    <div className="container flex gap-1 items-center flex-wrap">
+      {workflow?.steps?.map((step: any, index: number) => {
+        const provider = step?.provider;
+        if (["threshold", "assert", "foreach"].includes(provider?.type)) {
+          return null;
+        }
+        return provider ? (
+          <div
+            key={`step-${step.id}-${index}`}
+            className="flex items-center gap-1 flex-shrink-0"
+          >
+            {index > 0 && <TiArrowRight size={24} className="text-gray-500" />}
+            <DynamicImageProviderIcon
+              src={`/icons/${provider?.type}-icon.png`}
+              width={24}
+              height={24}
+              alt={provider?.type}
+              className="flex-shrink-0"
+            />
+          </div>
+        ) : null;
+      })}
+      {workflow?.actions?.map((action: any, index: number) => {
+        const provider = action?.provider;
+        if (["threshold", "assert", "foreach"].includes(provider?.type)) {
+          return null;
+        }
+        return provider ? (
+          <div
+            key={`action-${action.id}-${index}`}
+            className="flex items-center gap-1 flex-shrink-0"
+          >
+            {(index > 0 || isStepPresent) && (
+              <TiArrowRight size={24} className="text-gray-500" />
+            )}
+            <DynamicImageProviderIcon
+              src={`/icons/${provider?.type}-icon.png`}
+              width={24}
+              height={24}
+              alt={provider?.type}
+              className="flex-shrink-0"
+            />
+          </div>
+        ) : null;
+      })}
+    </div>
+  );
+}
diff --git a/keep-ui/app/(keep)/workflows/workflows-templates.tsx b/keep-ui/app/(keep)/workflows/workflows-templates.tsx
deleted file mode 100644
index b12faec734..0000000000
--- a/keep-ui/app/(keep)/workflows/workflows-templates.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import React, { useState } from "react";
-import {
-  MockStep,
-  MockWorkflow,
-  WorkflowTemplate,
-} from "@/shared/api/workflows";
-import { Button, Card } from "@tremor/react";
-import { useRouter } from "next/navigation";
-import { TiArrowRight } from "react-icons/ti";
-import Skeleton from "react-loading-skeleton";
-import "react-loading-skeleton/dist/skeleton.css";
-import useSWR from "swr";
-import { useApi } from "@/shared/lib/hooks/useApi";
-import { ErrorComponent } from "@/shared/ui";
-import { ArrowPathIcon } from "@heroicons/react/24/outline";
-import { DynamicImageProviderIcon } from "@/components/ui";
-
-export function WorkflowSteps({ workflow }: { workflow: MockWorkflow }) {
-  const isStepPresent =
-    !!workflow?.steps?.length &&
-    workflow?.steps?.find((step: MockStep) => step?.provider?.type);
-
-  return (
-    <div className="container flex gap-1 items-center flex-wrap">
-      {workflow?.steps?.map((step: any, index: number) => {
-        const provider = step?.provider;
-        if (["threshold", "assert", "foreach"].includes(provider?.type)) {
-          return null;
-        }
-        return provider ? (
-          <div
-            key={`step-${step.id}-${index}`}
-            className="flex items-center gap-1 flex-shrink-0"
-          >
-            {index > 0 && <TiArrowRight size={24} className="text-gray-500" />}
-            <DynamicImageProviderIcon
-              src={`/icons/${provider?.type}-icon.png`}
-              width={24}
-              height={24}
-              alt={provider?.type}
-              className="flex-shrink-0"
-            />
-          </div>
-        ) : null;
-      })}
-      {workflow?.actions?.map((action: any, index: number) => {
-        const provider = action?.provider;
-        if (["threshold", "assert", "foreach"].includes(provider?.type)) {
-          return null;
-        }
-        return provider ? (
-          <div
-            key={`action-${action.id}-${index}`}
-            className="flex items-center gap-1 flex-shrink-0"
-          >
-            {(index > 0 || isStepPresent) && (
-              <TiArrowRight size={24} className="text-gray-500" />
-            )}
-            <DynamicImageProviderIcon
-              src={`/icons/${provider?.type}-icon.png`}
-              width={24}
-              height={24}
-              alt={provider?.type}
-              className="flex-shrink-0"
-            />
-          </div>
-        ) : null;
-      })}
-    </div>
-  );
-}
-
-export function WorkflowTemplates() {
-  const api = useApi();
-  const router = useRouter();
-  const [loadingId, setLoadingId] = useState<string | null>(null);
-
-  /**
-    Add Mock Workflows (6 Random Workflows on Every Request)
-      To add mock workflows, a new backend API endpoint has been created: /workflows/random-templates.
-        1. Fetching Random Templates: When a request is made to this endpoint, all workflow YAML/YML files are read and
-           shuffled randomly.
-        2. Response: Only the first 6 files are parsed and sent in the response.
-   **/
-  const {
-    data: mockWorkflows,
-    error: mockError,
-    isLoading: mockLoading,
-    mutate: refresh,
-  } = useSWR<WorkflowTemplate[]>(
-    api.isReady() ? `/workflows/random-templates` : null,
-    (url: string) => api.get(url),
-    {
-      revalidateOnFocus: false,
-    }
-  );
-
-  const getNameFromId = (id: string) => {
-    if (!id) {
-      return "";
-    }
-    return id.split("-").join(" ");
-  };
-
-  const handlePreview = (template: WorkflowTemplate) => {
-    setLoadingId(template.workflow_raw_id);
-    localStorage.setItem("preview_workflow", JSON.stringify(template));
-    router.push(`/workflows/preview/${template.workflow_raw_id}`);
-  };
-
-  return (
-    <div className="w-full flex flex-col gap-6">
-      <h2 className="text-xl font-semibold flex justify-between items-baseline">
-        Discover workflow templates
-        <div className="flex justify-end">
-          <Button
-            icon={ArrowPathIcon}
-            color="orange"
-            size="sm"
-            onClick={() => {
-              refresh();
-            }}
-          >
-            Refresh
-          </Button>
-        </div>
-      </h2>
-
-      {/* TODO: Filters and search */}
-      {!mockLoading && !mockError && mockWorkflows?.length === 0 && (
-        <p className="text-center m-auto">No workflow templates found</p>
-      )}
-      {mockError && (
-        <ErrorComponent error={mockError} reset={() => refresh()} />
-      )}
-
-      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full gap-4">
-        {mockLoading && (
-          <>
-            {Array.from({ length: 8 }).map((_, index) => (
-              <div key={index} className="w-full h-48">
-                <Skeleton className="w-full h-48" />
-              </div>
-            ))}
-          </>
-        )}
-        {!mockLoading &&
-          mockWorkflows?.length &&
-          mockWorkflows?.map((template, index: number) => {
-            const workflow = template.workflow;
-            return (
-              <Card
-                key={index}
-                className="p-4 flex flex-col justify-between w-full border-2 border-transparent hover:border-orange-400 gap-2"
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-                  handlePreview(template);
-                }}
-              >
-                <div className="min-h-36">
-                  <WorkflowSteps workflow={workflow} />
-                  <h3 className="text-lg sm:text-xl font-semibold line-clamp-2">
-                    {getNameFromId(workflow.id)}
-                  </h3>
-                  <p className="mt-2 text-sm sm:text-base line-clamp-3">
-                    {workflow.description}
-                  </p>
-                </div>
-                <div>
-                  <Button
-                    variant="secondary"
-                    disabled={!!(loadingId && loadingId !== workflow.id)}
-                    loading={loadingId === workflow.id}
-                  >
-                    Preview
-                  </Button>
-                </div>
-              </Card>
-            );
-          })}
-      </div>
-    </div>
-  );
-}
diff --git a/keep-ui/app/(keep)/workflows/workflows.page.tsx b/keep-ui/app/(keep)/workflows/workflows.page.tsx
new file mode 100644
index 0000000000..d70fee312b
--- /dev/null
+++ b/keep-ui/app/(keep)/workflows/workflows.page.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { ErrorComponent, KeepLoader } from "@/shared/ui";
+import { useWorkflowsV2 } from "@/entities/workflows/model/useWorkflowsV2";
+import { InitialFacetsData } from "@/features/filter/api";
+import { ExistingWorkflowsState } from "./existing-workflows-state";
+import { NoWorkflowsState } from "./no-workflows-state";
+
+export function WorkflowsPage({
+  initialFacetsData,
+}: {
+  initialFacetsData?: InitialFacetsData;
+}) {
+  const { totalCount, error, isLoading } = useWorkflowsV2(
+    { cel: "", limit: 0, offset: 0 },
+    { keepPreviousData: true }
+  );
+
+  if (isLoading) {
+    return (
+      <div className="flex flex-col items-center justify-center h-full">
+        <KeepLoader />
+      </div>
+    );
+  }
+
+  if (error) {
+    return <ErrorComponent error={error} reset={() => {}} />;
+  }
+
+  if ((totalCount as number) > 0) {
+    return <ExistingWorkflowsState initialFacetsData={initialFacetsData} />;
+  }
+
+  return <NoWorkflowsState></NoWorkflowsState>;
+}
diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py
index 89ffb3e104..42666a3790 100644
--- a/keep/api/routes/workflows.py
+++ b/keep/api/routes/workflows.py
@@ -707,6 +707,43 @@ def get_random_workflow_templates(
     return workflows
 
 
+@router.post("/templates/query", description="Query workflow templates")
+def query_workflow_templates(
+    query: QueryDto,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:workflows"])
+    ),
+) -> dict:
+    tenant_id = authenticated_entity.tenant_id
+    workflowstore = WorkflowStore()
+    default_directory = os.environ.get(
+        "KEEP_WORKFLOWS_PATH",
+        os.path.join(os.path.dirname(__file__), "../../../examples/workflows"),
+    )
+    if not os.path.exists(default_directory):
+        # on the container we use the following path
+        fallback_directory = "/examples/workflows"
+        logger.warning(
+            f"{default_directory} does not exist, using fallback: {fallback_directory}"
+        )
+        if os.path.exists(fallback_directory):
+            default_directory = fallback_directory
+        else:
+            logger.error(f"Neither {default_directory} nor {fallback_directory} exist")
+            raise FileNotFoundError(
+                f"Neither {default_directory} nor {fallback_directory} exist"
+            )
+    workflows, total_count = workflowstore.query_workflow_templates(
+        tenant_id=tenant_id, workflows_dir=default_directory, query=query
+    )
+    return {
+        "limit": query.limit,
+        "offset": query.offset,
+        "count": total_count,
+        "results": workflows,
+    }
+
+
 @router.put(
     "/{workflow_id}",
     description="Update a workflow",
diff --git a/keep/workflowmanager/workflowstore.py b/keep/workflowmanager/workflowstore.py
index 20da9ab029..b6e71b5682 100644
--- a/keep/workflowmanager/workflowstore.py
+++ b/keep/workflowmanager/workflowstore.py
@@ -5,6 +5,7 @@
 import uuid
 from typing import Tuple
 
+import celpy
 import requests
 import validators
 from fastapi import HTTPException
@@ -21,6 +22,7 @@
 )
 from keep.api.core.workflows import get_workflows_with_last_executions_v2
 from keep.api.models.db.workflow import Workflow as WorkflowModel
+from keep.api.models.query import QueryDto
 from keep.api.models.workflow import PreparsedWorkflowDTO, ProviderDTO
 from keep.functions import cyaml
 from keep.parser.parser import Parser
@@ -32,6 +34,7 @@ class WorkflowStore:
     def __init__(self):
         self.parser = Parser()
         self.logger = logging.getLogger(__name__)
+        self.celpy_env = celpy.Environment()
 
     def get_workflow_execution(
         self,
@@ -535,6 +538,69 @@ def get_random_workflow_templates(
                 )
         return workflows
 
+    def query_workflow_templates(
+        self, tenant_id: str, workflows_dir: str, query: QueryDto
+    ) -> Tuple[list[dict], int]:
+        """
+        Get random workflows from a directory.
+        Args:
+            tenant_id (str): The tenant to which the workflows belong.
+            workflows_dir (str): A directory containing workflows yamls.
+            limit (int): The number of workflows to return.
+
+        Returns:
+            List[dict]: A list of workflows
+        """
+        if not os.path.isdir(workflows_dir):
+            raise FileNotFoundError(f"Directory {workflows_dir} does not exist")
+
+        workflow_yaml_files = [
+            f for f in os.listdir(workflows_dir) if f.endswith((".yaml", ".yml"))
+        ]
+        if not workflow_yaml_files:
+            raise FileNotFoundError(f"No workflows found in directory {workflows_dir}")
+
+        workflows = []
+
+        for file in workflow_yaml_files:
+            try:
+                file_path = os.path.join(workflows_dir, file)
+                workflow_yaml = self._parse_workflow_to_dict(file_path)
+                if "workflow" in workflow_yaml:
+                    workflow_yaml["name"] = workflow_yaml["workflow"]["id"]
+                    workflow_yaml["workflow_raw"] = cyaml.dump(workflow_yaml)
+                    workflow_yaml["workflow_raw_id"] = workflow_yaml["workflow"]["id"]
+
+                    if not query.cel:
+                        workflows.append(workflow_yaml)
+                        continue
+
+                    ast = self.celpy_env.compile(query.cel)
+                    prgm = self.celpy_env.program(ast)
+
+                    activation = celpy.json_to_cel(
+                        {
+                            "name": workflow_yaml.get("workflow", {})
+                            .get("name", None)
+                            .lower(),
+                            "description": workflow_yaml.get("workflow", {})
+                            .get("description", "")
+                            .lower(),
+                        }
+                    )
+                    relevant = prgm.evaluate(activation)
+
+                    if relevant:
+                        workflows.append(workflow_yaml)
+
+                self.logger.info(f"Workflow from {file} fetched successfully")
+            except Exception as e:
+                self.logger.error(
+                    f"Error parsing or fetching workflow from {file}: {e}"
+                )
+
+        return workflows[query.offset : query.offset + query.limit], len(workflows)
+
     def group_last_workflow_executions(self, workflows: list[dict]) -> list[dict]:
         """
         Group last workflow executions by workflow id


Code review comments:
diff_hunk: @@ -0,0 +1,35 @@
+import { WorkflowTemplate } from "@/shared/api/workflows";
+import { useApi } from "@/shared/lib/hooks/useApi";
+import useSWR, { SWRConfiguration } from "swr";
+
+export interface WorkflowTemplatesQuery {
+  cel: string;
+  limit: number;
+  offset: number;
+}
+
+export function useQueryWorkflowTemplate(
+  query: WorkflowTemplatesQuery,
+  options?: SWRConfiguration<any>
+) {
+  const api = useApi();
+  const requestUrl = `/workflows/templates/query`;
+  const { data, error, isLoading, mutate } = useSWR<any>(
+    api.isReady() && query
path: keep-ui/app/(keep)/workflows/create-workflow-modal/use-query-workflow-template.ts
text: @user1:
also, here can you use the patter from https://github.com/keephq/keep/blob/main/keep-ui/entities/workflows/model/workflowKeys.ts for keys

diff_hunk: @@ -0,0 +1,56 @@
+import { WorkflowSteps } from "../workflows-steps";
+import { WorkflowTemplate } from "@/shared/api/workflows";
+import { Button, Card } from "@tremor/react";
+import { useRouter } from "next/navigation";
+import Skeleton from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
+
+export const WorkflowTemplateCard: React.FC<{ template: WorkflowTemplate }> = ({
+  template,
+}) => {
+  const router = useRouter();
+  const handlePreview = (template: WorkflowTemplate) => {
+    localStorage.setItem("preview_workflow", JSON.stringify(template));
+    router.push(`/workflows/preview/${template.workflow_raw_id}`);
+  };
+  const getNameFromId = (id: string) => {
+    if (!id) {
+      return "";
+    }
+    return id.split("-").join(" ");
+  };
+  return (
+    <Card
+      className="p-4 flex flex-col justify-between w-full border-2 border-transparent hover:border-orange-400 gap-2"
+      onClick={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        handlePreview(template);
+      }}
+    >
+      <div className="min-h-36">
+        {template && <WorkflowSteps workflow={template.workflow} />}
+        {!template && <Skeleton className="h-4 w-16 mb-2" />}
+        <h3 className="text-lg sm:text-xl font-semibold line-clamp-2">
+          {template && getNameFromId(template.workflow.id)}
+          {!template && <Skeleton className="h-4 w-28 mb-2" />}
+        </h3>
+        <p className="mt-2 text-sm sm:text-base line-clamp-3">
+          {template && template.workflow.description}
+          {!template && <Skeleton className="h-16 w-full mb-2" />}
+        </p>
+      </div>
+      <div>
+        {template && (
+          <Button
+            variant="secondary"
+            //   disabled={!!(loadingId && loadingId !== workflow.id)}
path: keep-ui/app/(keep)/workflows/create-workflow-modal/workflow-template-card.tsx
text: @user1:
leftover?