Repository: keephq/keep
Pull request: fix: dummy workflow per tenant
<!-- 
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 #4599 

Problem statement:
[🐛 Bug]: system-test-workflow breaks tenant isolation
## Problem
To maintain FK contraints all workflow test runs assigned to `system-test-workflow` on `system-test-workflow` tenant. It breaks multi tenancy - two tenants will have dummy workflows on the same tenant.

## Solution

### Simpliest
For now, create dummy workflow `system-test-workflow` for each tenant.

### Another option
Create `draft` workflows for test runs and show them in the UI under facet "drafts", turned off by default.

Hints from issue discussion:

Commit message: fix: delete deprecated dummy workflow in migration

Patch to review:
diff --git a/keep-ui/features/workflows/test-run/ui/workflow-test-run-button.tsx b/keep-ui/features/workflows/test-run/ui/workflow-test-run-button.tsx
index 09efb03d93..12e292fba5 100644
--- a/keep-ui/features/workflows/test-run/ui/workflow-test-run-button.tsx
+++ b/keep-ui/features/workflows/test-run/ui/workflow-test-run-button.tsx
@@ -38,6 +38,10 @@ export function WorkflowTestRunButton({
     const workflow = getYamlWorkflowDefinition(definition.value);
     // NOTE: prevent the workflow from being disabled, so test run doesn't fail
     workflow.disabled = false;
+    if (workflowId) {
+      // if existing workflow, use it's real id for test run
+      workflow.id = workflowId;
+    }
     const body = getBodyFromStringOrDefinitionOrObject({
       workflow,
     });
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 80e4fa8937..1fbd20b79c 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -13,7 +13,7 @@
 from contextlib import contextmanager
 from datetime import datetime, timedelta, timezone
 from functools import wraps
-from typing import Any, Callable, Dict, List, Tuple, Type, Union
+from typing import Any, Callable, Dict, Iterator, List, Tuple, Type, Union
 from uuid import UUID, uuid4
 
 import validators
@@ -47,7 +47,11 @@
 
 from keep.api.consts import STATIC_PRESETS
 from keep.api.core.config import config
-from keep.api.core.db_utils import create_db_engine, get_json_extract_field
+from keep.api.core.db_utils import (
+    create_db_engine,
+    get_json_extract_field,
+    get_or_create,
+)
 from keep.api.core.dependencies import SINGLE_TENANT_UUID
 
 # This import is required to create the tables
@@ -112,7 +116,7 @@ def dispose_session():
 
 
 @contextmanager
-def existed_or_new_session(session: Optional[Session] = None) -> Session:
+def existed_or_new_session(session: Optional[Session] = None) -> Iterator[Session]:
     try:
         if session:
             yield session
@@ -207,7 +211,7 @@ def create_workflow_execution(
                 triggered_by = triggered_by[:255]
             workflow_execution = WorkflowExecution(
                 id=workflow_execution_id,
-                workflow_id=workflow_id if not test_run else "test",
+                workflow_id=workflow_id,
                 workflow_revision=workflow_revision,
                 tenant_id=tenant_id,
                 started=datetime.now(tz=timezone.utc),
@@ -217,7 +221,7 @@ def create_workflow_execution(
                 error=None,
                 execution_time=None,
                 results={},
-                # is_test_run=test_run,
+                is_test_run=test_run,
             )
             session.add(workflow_execution)
             # Ensure the object has an id
@@ -275,6 +279,7 @@ def get_last_completed_execution(
     return session.exec(
         select(WorkflowExecution)
         .where(WorkflowExecution.workflow_id == workflow_id)
+        .where(WorkflowExecution.is_test_run == False)
         .where(
             (WorkflowExecution.status == "success")
             | (WorkflowExecution.status == "error")
@@ -537,6 +542,7 @@ def add_or_update_workflow(
     updated_by: str,
     provisioned: bool = False,
     provisioned_file: str | None = None,
+    is_test: bool = False,
 ) -> Workflow:
     with Session(engine, expire_on_commit=False) as session:
         # TODO: we need to better understanad if that's the right behavior we want
@@ -557,6 +563,7 @@ def add_or_update_workflow(
             )
 
         else:
+            now = datetime.now(tz=timezone.utc)
             # Create a new workflow
             workflow = Workflow(
                 id=id,
@@ -566,11 +573,13 @@ def add_or_update_workflow(
                 description=description,
                 created_by=created_by,
                 updated_by=updated_by,
+                last_updated=now,
                 interval=interval,
                 is_disabled=is_disabled,
                 workflow_raw=workflow_raw,
                 provisioned=provisioned,
                 provisioned_file=provisioned_file,
+                is_test=is_test,
             )
             version = WorkflowVersion(
                 workflow_id=workflow.id,
@@ -578,6 +587,9 @@ def add_or_update_workflow(
                 workflow_raw=workflow_raw,
                 updated_by=updated_by,
                 comment=f"Created by {created_by}",
+                is_valid=True,
+                is_current=True,
+                updated_at=now,
             )
             session.add(workflow)
             session.add(version)
@@ -585,6 +597,30 @@ def add_or_update_workflow(
             return workflow
 
 
+def get_or_create_dummy_workflow(tenant_id: str, session: Session | None = None):
+    with existed_or_new_session(session) as session:
+        workflow, created = get_or_create(
+            session,
+            Workflow,
+            tenant_id=tenant_id,
+            id=get_dummy_workflow_id(tenant_id),
+            name="Dummy Workflow for test runs",
+            description="Auto-generated dummy workflow for test runs",
+            created_by="system",
+            workflow_raw="{}",
+            is_disabled=False,
+            is_test=True,
+        )
+        if created:
+            # For new instances, make sure they're committed and refreshed from the database
+            session.commit()
+            session.refresh(workflow)
+        elif workflow:
+            # For existing instances, refresh to get the current state
+            session.refresh(workflow)
+        return workflow
+
+
 def get_workflow_to_alert_execution_by_workflow_execution_id(
     workflow_execution_id: str,
 ) -> WorkflowToAlertExecution:
@@ -711,6 +747,7 @@ def get_workflows_with_last_execution(tenant_id: str) -> List[dict]:
             )
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.is_deleted == False)
+            .where(Workflow.is_test == False)
         ).distinct()
 
         result = session.execute(workflows_with_last_execution_query).all()
@@ -723,6 +760,7 @@ def get_all_workflows(tenant_id: str):
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.is_deleted == False)
+            .where(Workflow.is_test == False)
         ).all()
     return workflows
 
@@ -734,6 +772,7 @@ def get_all_provisioned_workflows(tenant_id: str):
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.provisioned == True)
             .where(Workflow.is_deleted == False)
+            .where(Workflow.is_test == False)
         ).all()
     return list(workflows)
 
@@ -754,6 +793,7 @@ def get_all_workflows_yamls(tenant_id: str):
             select(Workflow.workflow_raw)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.is_deleted == False)
+            .where(Workflow.is_test == False)
         ).all()
     return list(workflows)
 
@@ -764,6 +804,7 @@ def get_workflow(tenant_id: str, workflow_id: str):
             select(Workflow)
             .where(Workflow.tenant_id == tenant_id)
             .where(Workflow.is_deleted == False)
+            .where(Workflow.is_test == False)
         )
         if validators.uuid(workflow_id):
             query = query.where(Workflow.id == workflow_id)
@@ -893,6 +934,7 @@ def get_workflow_executions(
     status: Optional[Union[str, List[str]]] = None,
     trigger: Optional[Union[str, List[str]]] = None,
     execution_id: Optional[str] = None,
+    is_test_run: bool = False,
 ):
     with Session(engine) as session:
         query = session.query(
@@ -900,6 +942,7 @@ def get_workflow_executions(
         ).filter(
             WorkflowExecution.tenant_id == tenant_id,
             WorkflowExecution.workflow_id == workflow_id,
+            WorkflowExecution.is_test_run == False,
         )
 
         now = datetime.now(tz=timezone.utc)
@@ -1063,21 +1106,24 @@ def push_logs_to_db(log_entries):
         session.commit()
 
 
-def get_workflow_execution(tenant_id: str, workflow_execution_id: str):
+def get_workflow_execution(
+    tenant_id: str, workflow_execution_id: str, is_test_run: bool | None = None
+):
     with Session(engine) as session:
-        execution_with_logs = (
-            session.query(WorkflowExecution)
-            .filter(
-                WorkflowExecution.id == workflow_execution_id,
-                WorkflowExecution.tenant_id == tenant_id,
-            )
-            .options(
-                joinedload(WorkflowExecution.logs),
-                joinedload(WorkflowExecution.workflow_to_alert_execution),
-                joinedload(WorkflowExecution.workflow_to_incident_execution),
+        base_query = session.query(WorkflowExecution)
+        if is_test_run is not None:
+            base_query = base_query.filter(
+                WorkflowExecution.is_test_run == is_test_run,
             )
-            .one()
+        base_query = base_query.filter(
+            WorkflowExecution.id == workflow_execution_id,
+            WorkflowExecution.tenant_id == tenant_id,
         )
+        execution_with_logs = base_query.options(
+            joinedload(WorkflowExecution.logs),
+            joinedload(WorkflowExecution.workflow_to_alert_execution),
+            joinedload(WorkflowExecution.workflow_to_incident_execution),
+        ).one()
     return execution_with_logs
 
 
@@ -2010,6 +2056,7 @@ def get_previous_execution_id(tenant_id, workflow_id, workflow_execution_id):
             .where(WorkflowExecution.tenant_id == tenant_id)
             .where(WorkflowExecution.workflow_id == workflow_id)
             .where(WorkflowExecution.id != workflow_execution_id)
+            .where(WorkflowExecution.is_test_run == False)
             .where(
                 WorkflowExecution.started >= datetime.now() - timedelta(days=1)
             )  # no need to check more than 1 day ago
diff --git a/keep/api/core/db_utils.py b/keep/api/core/db_utils.py
index b25a511347..4831131b7e 100644
--- a/keep/api/core/db_utils.py
+++ b/keep/api/core/db_utils.py
@@ -7,6 +7,7 @@
 import json
 import logging
 import os
+from typing import Any, Dict, Optional, Type, TypeVar, Tuple
 
 import pymysql
 from dotenv import find_dotenv, load_dotenv
@@ -15,7 +16,8 @@
 from sqlalchemy.ext.compiler import compiles
 from sqlalchemy.sql.ddl import CreateColumn
 from sqlalchemy.sql.functions import GenericFunction
-from sqlmodel import Session, create_engine
+from sqlmodel import Session, create_engine, SQLModel, select
+from sqlalchemy.exc import IntegrityError
 
 # This import is required to create the tables
 from keep.api.consts import RUNNING_IN_CLOUD_RUN
@@ -208,3 +210,59 @@ def _compile_json_table(element, compiler, **kw):
             for clause in element.clauses.clauses[1:]
         ),
     )
+
+
+T = TypeVar("T", bound=SQLModel)
+
+
+def get_or_create(
+    session: Session,
+    model: Type[T],
+    defaults: Optional[Dict[str, Any]] = None,
+    **kwargs: Any,
+) -> Tuple[T, bool]:
+    """
+    Get an instance by filter kwargs, or create one with those filters plus any defaults.
+
+    Args:
+        session: SQLModel session
+        model: Model class
+        defaults: Dict of default values for creation (not used for lookup)
+        **kwargs: Filter parameters used both for lookup and creation
+
+    Returns:
+        tuple: (instance, created) where created is a boolean indicating if a new instance was created
+    """
+    # Build query with all filter conditions
+    query = select(model)
+    for key, value in kwargs.items():
+        query = query.where(getattr(model, key) == value)
+
+    # Execute the query
+    instance = session.exec(query).first()
+
+    if instance:
+        return instance, False
+
+    # Prepare creation attributes
+    create_attrs = kwargs.copy()
+    if defaults:
+        create_attrs.update(defaults)
+
+    instance = model(**create_attrs)
+    session.add(instance)
+
+    try:
+        # Try to flush without committing to detect any integrity errors
+        session.flush()
+        return instance, True
+    except IntegrityError:
+        # If there's a conflict, roll back and try to fetch again (another process might have created it)
+        session.rollback()
+
+        # Try to fetch again with the same query
+        instance = session.exec(query).first()
+        if instance:
+            return instance, False
+        # If we still can't find it, something else is wrong, re-raise
+        raise
diff --git a/keep/api/core/workflows.py b/keep/api/core/workflows.py
index 90e96aa424..46bfd985e2 100644
--- a/keep/api/core/workflows.py
+++ b/keep/api/core/workflows.py
@@ -215,6 +215,7 @@ def build_workflows_query(
             and_(
                 Workflow.id == literal_column("entity_id"),
                 Workflow.tenant_id == tenant_id,
+                Workflow.is_test == False,
             ),
         )
         .outerjoin(
diff --git a/keep/api/models/db/migrations/versions/2025-04-21-10-18_819927b7ccfa.py b/keep/api/models/db/migrations/versions/2025-04-21-10-18_819927b7ccfa.py
new file mode 100644
index 0000000000..d1af2a2294
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2025-04-21-10-18_819927b7ccfa.py
@@ -0,0 +1,97 @@
+"""workflow is_test and workflowexecution is_test_run columns
+
+Revision ID: 819927b7ccfa
+Revises: 885ff6b12fed
+Create Date: 2025-04-21 10:18:49.074198
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "819927b7ccfa"
+down_revision = "885ff6b12fed"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+
+    with op.batch_alter_table("workflow", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column(
+                "is_test",
+                sa.Boolean(),
+                nullable=False,
+                server_default=sa.false(),
+            )
+        )
+
+    with op.batch_alter_table("workflowexecution", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column(
+                "is_test_run",
+                sa.Boolean(),
+                nullable=False,
+                server_default=sa.false(),
+            )
+        )
+
+    # Delete all related data for deprecated system test workflow tenant, workflow and related workflowexecutions, workfloecexutionlog, etc
+
+    # First delete workflow execution logs
+    op.execute(
+        """
+        DELETE FROM workflowexecutionlog 
+        WHERE workflow_execution_id IN (
+            SELECT id FROM workflowexecution WHERE workflow_id = 'test'
+        )
+    """
+    )
+
+    # Delete workflow-to-alert relations
+    op.execute(
+        """
+        DELETE FROM workflowtoalertexecution
+        WHERE workflow_execution_id IN (
+            SELECT id FROM workflowexecution WHERE workflow_id = 'test'
+        )
+    """
+    )
+
+    # Delete workflow-to-incident relations
+    op.execute(
+        """
+        DELETE FROM workflowtoincidentexecution
+        WHERE workflow_execution_id IN (
+            SELECT id FROM workflowexecution WHERE workflow_id = 'test'
+        )
+    """
+    )
+
+    # Delete workflow executions
+    op.execute("DELETE FROM workflowexecution WHERE workflow_id = 'test'")
+
+    # Delete workflow version
+    op.execute("DELETE FROM workflowversion WHERE workflow_id = 'test'")
+
+    # Delete the test workflow
+    op.execute("DELETE FROM workflow WHERE id = 'test'")
+
+    # Finally delete the system test tenant
+    op.execute("DELETE FROM tenant WHERE id = 'system-test-workflow'")
+
+    # ### 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_column("is_test_run")
+
+    with op.batch_alter_table("workflow", schema=None) as batch_op:
+        batch_op.drop_column("is_test")
+
+    # ### end Alembic commands ###
diff --git a/keep/api/models/db/workflow.py b/keep/api/models/db/workflow.py
index f133922cfe..900fb6bed3 100644
--- a/keep/api/models/db/workflow.py
+++ b/keep/api/models/db/workflow.py
@@ -5,6 +5,10 @@
 from sqlmodel import JSON, Column, Field, Relationship, SQLModel, UniqueConstraint
 
 
+def get_dummy_workflow_id(tenant_id: str) -> str:
+    return f"system-dummy-workflow-{tenant_id}"
+
+
 class Workflow(SQLModel, table=True):
     id: str = Field(default=None, primary_key=True)
     tenant_id: str = Field(foreign_key="tenant.id")
@@ -29,6 +33,7 @@ class Workflow(SQLModel, table=True):
     )
     provisioned: bool = Field(default=False)
     provisioned_file: Optional[str] = None
+    is_test: bool = Field(default=False)
 
     executions: List["WorkflowExecution"] = Relationship(back_populates="workflow")
     versions: List["WorkflowVersion"] = Relationship(back_populates="workflow")
@@ -134,6 +139,7 @@ class WorkflowExecution(SQLModel, table=True):
     error: Optional[str] = Field(max_length=10240)
     execution_time: Optional[int]
     results: dict = Field(sa_column=Column(JSON), default={})
+    is_test_run: bool = Field(default=False)
 
     workflow: "Workflow" = Relationship(
         back_populates="executions",
diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py
index 9487525ade..d05a9d39a2 100644
--- a/keep/api/routes/workflows.py
+++ b/keep/api/routes/workflows.py
@@ -25,6 +25,7 @@
     get_alert_by_event_id,
     get_installed_providers,
     get_last_workflow_workflow_to_alert_executions,
+    get_or_create_dummy_workflow,
     get_session,
     get_workflow,
     get_workflow_version,
@@ -476,10 +477,31 @@ async def run_workflow_from_definition(
     workflow_dict = await get_workflow_dict_from_string(workflow_raw)
     workflowstore = WorkflowStore()
     workflowmanager = WorkflowManager.get_instance()
+    workflow_id = workflow_dict.get("id")
+
+    if workflow_id:
+        # if workflow exists, use it's id for test run
+        try:
+            workflow_from_db = workflowstore.get_workflow(tenant_id, workflow_id)
+            workflow_id = workflow_from_db.workflow_id
+        except HTTPException:
+            # if workflow_id is not found, use dummy workflow id for test run
+            workflow_id = None
+    if workflow_id is None:
+        # otherwise, ensure dummy workflow exists and use it's id for test run
+        try:
+            dummy_workflow = get_or_create_dummy_workflow(tenant_id)
+            workflow_id = dummy_workflow.id
+        except Exception as e:
+            logger.exception(
+                "Failed to create dummy workflow",
+                extra={"tenant_id": tenant_id},
+            )
+            raise HTTPException(
+                status_code=500, detail=f"Failed to create dummy workflow: {e}"
+            )
     try:
-        workflow = workflowstore.get_workflow_from_dict(
-            tenant_id=tenant_id, workflow_dict=workflow_dict
-        )
+        workflow = workflowstore.get_workflow_from_dict(tenant_id, workflow_dict)
     except Exception as e:
         logger.exception(
             "Failed to parse workflow",
@@ -493,7 +515,7 @@ async def run_workflow_from_definition(
     try:
         event, inputs = get_event_from_body(body, tenant_id)
         workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow(
-            workflow.workflow_id,
+            workflow_id,
             workflow.workflow_revision,
             tenant_id,
             created_by,
diff --git a/keep/parser/parser.py b/keep/parser/parser.py
index 6bf748ad9e..3a6e879723 100644
--- a/keep/parser/parser.py
+++ b/keep/parser/parser.py
@@ -61,6 +61,7 @@ def parse(
         actions_file: str = None,
         workflow_db_id: str = None,
         workflow_revision: int = None,
+        is_test: bool = False,
     ) -> typing.List[Workflow]:
         """_summary_
 
@@ -88,6 +89,7 @@ def parse(
                     actions_file,
                     workflow_actions,
                     workflow_db_id,
+                    is_test,
                 )
                 for workflow in raw_workflows
             ]
@@ -104,6 +106,8 @@ def parse(
                 workflow_providers,
                 actions_file,
                 workflow_actions,
+                workflow_db_id,
+                is_test,
             )
             workflows = [workflow]
         # else, if it stored in the db, it stored without the "workflow" key
@@ -117,6 +121,7 @@ def parse(
                 actions_file,
                 workflow_actions,
                 workflow_db_id=workflow_db_id,
+                is_test=is_test,
             )
             workflows = [workflow]
         return workflows
@@ -148,6 +153,7 @@ def _parse_workflow(
         actions_file: str = None,
         workflow_actions: dict = None,
         workflow_db_id: str = None,
+        is_test: bool = False,
     ) -> Workflow:
         self.logger.debug("Parsing workflow")
         # @tb: we need to remove this id in workflow yaml, it has no real use.
@@ -164,7 +170,6 @@ def _parse_workflow(
         self._load_actions_config(
             tenant_id, context_manager, workflow, actions_file, workflow_actions
         )
-        workflow_id = self._parse_id(workflow)
         workflow_name = workflow.get("name", "Untitled")
         workflow_description = workflow.get("description", "No description")
         workflow_permissions = workflow.get("permissions", [])
@@ -210,6 +215,7 @@ def _parse_workflow(
             workflow_consts=workflow_consts,
             workflow_debug=workflow_debug,
             workflow_permissions=workflow_permissions,
+            is_test=is_test,
         )
         self.logger.debug("Workflow parsed successfully")
         return workflow_class
diff --git a/keep/workflowmanager/workflow.py b/keep/workflowmanager/workflow.py
index 8d6c89ae04..16ca7dce22 100644
--- a/keep/workflowmanager/workflow.py
+++ b/keep/workflowmanager/workflow.py
@@ -40,6 +40,7 @@ def __init__(
         workflow_consts: typing.Dict[str, str] = {},
         workflow_debug: bool = False,
         workflow_permissions: typing.List[str] = [],
+        is_test: bool = False,
     ):
         self.workflow_id = workflow_id
         self.workflow_revision = workflow_revision
@@ -56,6 +57,7 @@ def __init__(
         self.workflow_providers_type = workflow_providers_type
         self.workflow_strategy = workflow_strategy
         self.workflow_consts = workflow_consts
+        self.is_test = is_test
         self.on_failure = on_failure
         self.context_manager = context_manager
         self.context_manager.set_consts_context(workflow_consts)
diff --git a/keep/workflowmanager/workflowstore.py b/keep/workflowmanager/workflowstore.py
index 74faf197c7..9ec61102bb 100644
--- a/keep/workflowmanager/workflowstore.py
+++ b/keep/workflowmanager/workflowstore.py
@@ -33,8 +33,13 @@ def __init__(self):
         self.parser = Parser()
         self.logger = logging.getLogger(__name__)
 
-    def get_workflow_execution(self, tenant_id: str, workflow_execution_id: str):
-        return get_workflow_execution(tenant_id, workflow_execution_id)
+    def get_workflow_execution(
+        self,
+        tenant_id: str,
+        workflow_execution_id: str,
+        is_test_run: bool | None = None,
+    ):
+        return get_workflow_execution(tenant_id, workflow_execution_id, is_test_run)
 
     def create_workflow(self, tenant_id: str, created_by, workflow: dict):
         workflow_id = workflow.get("id")
@@ -121,8 +126,9 @@ def get_workflow(self, tenant_id: str, workflow_id: str) -> Workflow:
         workflow = self.parser.parse(
             tenant_id,
             workflow_yaml,
-            workflow_db_id=workflow_id,
+            workflow_db_id=workflow.id,
             workflow_revision=workflow.revision,
+            is_test=workflow.is_test,
         )
         if len(workflow) > 1:
             raise HTTPException(


Code review comments:
diff_hunk: @@ -476,10 +477,31 @@ async def run_workflow_from_definition(
     workflow_dict = await get_workflow_dict_from_string(workflow_raw)
     workflowstore = WorkflowStore()
     workflowmanager = WorkflowManager.get_instance()
+    workflow_id = workflow_dict.get("id")
+
+    if workflow_id:
+        # if workflow exists, use it's id for test run
+        try:
+            workflow_from_db = workflowstore.get_workflow(tenant_id, workflow_id)
+            workflow_id = workflow_from_db.workflow_id
path: keep/api/routes/workflows.py
text: @user1:
logic: workflow_id is redundantly assigned to workflow_from_db.workflow_id - they should be the same value

```suggestion
            workflow_id = workflow_from_db.id
```