Repository: keephq/keep
Pull request: fix: incident enrichments
<!-- 
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 #5097 

# Problem
When enriching incident via workflow, python-side str(uuid) is called to get `alert_fingerprint` for `alertenrichment` entry. 

https://github.com/keephq/keep/blob/v0.45.11/keep/providers/base/base_provider.py#L244-L245

Therefore, when we look up incident enrichments using cast(alert_fingerprint as char) no enrichments will be found, because python will convert uuid to string with dashes, while mysql will convert to string without dashes:

https://github.com/keephq/keep/blob/v0.45.11/keep/api/core/db.py#L3979-L3990

# Summary

- Allow UUID `fingerprint` in `enrichments_bl.enrich_entity` to save uuid in db-specific way.
- Removed migration since for alerts fingerprints always stored as string and migration will can cause incinsistency.  ~~Added migration `2025-07-01-12-19_alertenrichment_uuid_dashes.py` to standardize UUID formatting across database dialects by removing dashes.~~
- Test-related changes:
  - Added threading.lock() in workflowmanager.get_instance() to ensure we always will have an instance with scheduler


Problem statement:
[🐛 Bug]: Incident made enrichments via workflow aren't visible when using mysql
**To Reproduce**
Steps to reproduce the behavior:

1. Create a workflow:
```yaml
workflow:
  id: incident-metadata-enricher
  name: Incident Metadata Enricher
  description: Enriches incidents with additional metadata including environment, incident IDs, URLs, and provider information while logging incident details.
  disabled: false
  triggers:
    - type: manual
    - events:
        - created
        - updated
      type: incident
  consts: {}
  owners: []
  services: []
  steps: []
  actions:
    - name: mock-step
      provider:
        type: mock
        with:
          enrich_incident:
            - key: jira_ticket
              value: "12345"
```
2. Create an incident.
3. Go to incident detail page and see that no jira_ticket field is visible.
4. See table alertenrichment in mysql has the enrichments. 

**Expected behavior**
Enrichments are visible, like when added manually via `add field` button on detail view.

Hints from issue discussion:
<!-- Answer -->
The enrichment you add via workflow is being stored in the database, but it's not showing up in the UI—unlike enrichments added manually. Based on recent code and PRs, the backend uses the same enrichment logic for both workflow and manual actions, so the difference is likely in how the UI fetches or filters enrichments for display. There was a recent change that made special ticket fields (`ticket_url`, `ticket_id`) show up as links, but generic fields like `jira_ticket` aren't handled the same way and may not be surfaced in the UI by default [see here](https://github.com/keephq/keep/pull/4995).

It's worth comparing the structure of enrichments created by workflows vs. manual actions in your database—there could be a subtle difference (like a missing type or flag) that causes the UI to skip them. Also, check if the API response for incident details includes your workflow enrichment. If not, the issue is likely in the backend query or serialization logic. If it does, then the UI might be filtering it out.

No MySQL-specific caveats or documentation were found about this behavior. Reviewing the incident enrichment API and the UI's enrichment rendering logic should help pinpoint the gap [reference](https://github.com/keephq/keep/blob/33eddb2ba8a950a0abb4b4d21dfc3cac00fb6d20/keep/api/routes/incidents.py).


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fd98c932-a51e-405e-9a9e-449d7ba2e2d3?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/keephq/keep/issues/5097)

Commit message: fix: add migration

Patch to review:
diff --git a/keep/api/bl/enrichments_bl.py b/keep/api/bl/enrichments_bl.py
index 2d6894fc37..44d22f866c 100644
--- a/keep/api/bl/enrichments_bl.py
+++ b/keep/api/bl/enrichments_bl.py
@@ -722,7 +722,7 @@ def disposable_enrich_entity(
 
     def enrich_entity(
         self,
-        fingerprint: str,
+        fingerprint: str | UUID,
         enrichments: dict,
         action_type: ActionType,
         action_callee: str,
@@ -740,6 +740,10 @@ def enrich_entity(
         Enrich the alert with extraction and mapping rules
         """
         # enrich db
+        if isinstance(fingerprint, UUID):
+            fingerprint = UUIDType(binary=False).process_bind_param(
+                fingerprint, self.db_session.bind.dialect
+            )
         self.logger.debug(
             "enriching alert db",
             extra={"fingerprint": fingerprint, "tenant_id": self.tenant_id},
diff --git a/keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py b/keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py
new file mode 100644
index 0000000000..a3bfeffeef
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py
@@ -0,0 +1,45 @@
+"""Remove dashes from alert_fingerprint UUIDs for mysql and sqlite
+
+Revision ID: 613f4a584f37
+Revises: 9dd1be4539e0
+Create Date: 2025-07-01 12:19:08.895646
+
+We look up incident enrichments using cast(alert_fingerprint as char). MySQL casts uuid to varchar without dashes. However, until now we used Python's `str()` to cast the uuid to a string when creating the incident enrichment, resulting in enrichments not found. This migration removes the dashes from previous entries.
+
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "613f4a584f37"
+down_revision = "9dd1be4539e0"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    dialect = op.get_context().dialect.name
+    if dialect == "postgresql":
+        pass
+    elif dialect == "mysql":
+        # for mysql find all uuid strings and replace the dashes with empty strings, as mysql casts uuid to varchar without dashes,
+        op.execute(
+            """UPDATE IGNORE alertenrichment 
+            SET alert_fingerprint = REPLACE(alert_fingerprint, '-', '')
+            WHERE CHAR_LENGTH(alert_fingerprint) = 36 
+            AND alert_fingerprint REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
+            """
+        )
+    elif dialect == "sqlite":
+        op.execute(
+            """UPDATE or IGNORE alertenrichment 
+            SET alert_fingerprint = REPLACE(alert_fingerprint, '-', '')
+            WHERE LENGTH(alert_fingerprint) = 36 
+            AND alert_fingerprint REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
+            """
+        )
+    pass
+
+
+def downgrade() -> None:
+    pass
diff --git a/keep/api/routes/incidents.py b/keep/api/routes/incidents.py
index 27c37045aa..f097483c7f 100644
--- a/keep/api/routes/incidents.py
+++ b/keep/api/routes/incidents.py
@@ -1,5 +1,6 @@
 import logging
 from typing import List, Optional
+from uuid import UUID
 
 from arq import ArqRedis
 from fastapi import (
@@ -13,8 +14,6 @@
     Response,
 )
 from pusher import Pusher
-from pydantic.types import UUID
-from sqlalchemy_utils import UUIDType
 from sqlmodel import Session
 
 from keep.api.arq_pool import get_pool
@@ -36,7 +35,8 @@
     get_rule,
     get_session,
     get_workflow_executions_for_incident_or_alert,
-    merge_incidents_to_id, get_enrichment,
+    merge_incidents_to_id,
+    get_enrichment,
 )
 from keep.api.core.dependencies import extract_generic_body, get_pusher_client
 from keep.api.core.incidents import (
@@ -45,7 +45,11 @@
     get_incident_potential_facet_fields,
 )
 from keep.api.models.action_type import ActionType
-from keep.api.models.alert import AlertDto, EnrichIncidentRequestBody, UnEnrichIncidentRequestBody
+from keep.api.models.alert import (
+    AlertDto,
+    EnrichIncidentRequestBody,
+    UnEnrichIncidentRequestBody,
+)
 from keep.api.models.db.alert import (
     AlertAudit,
     CommentMention,
@@ -903,7 +907,7 @@ def add_comment(
         "commenter": authenticated_entity.email,
         "comment": change.comment,
         "incident_id": str(incident_id),
-        "tagged_users": change.tagged_users
+        "tagged_users": change.tagged_users,
     }
     logger.info("Adding comment to incident", extra=extra)
     comment = add_audit(
@@ -913,7 +917,7 @@ def add_comment(
         ActionType.INCIDENT_COMMENT,
         change.comment,
         session=session,
-        commit=False
+        commit=False,
     )
 
     if change.tagged_users:
@@ -921,10 +925,10 @@ def add_comment(
             mention = CommentMention(
                 comment_id=comment.id,
                 mentioned_user_id=user_email,
-                tenant_id=authenticated_entity.tenant_id
+                tenant_id=authenticated_entity.tenant_id,
             )
             session.add(mention)
-    
+
     session.commit()
     session.refresh(comment)
 
@@ -1066,12 +1070,9 @@ async def enrich_incident(
 
     # Use the existing enrichment infrastructure
     enrichment_bl = EnrichmentsBl(tenant_id, db_session)
-    fingerprint = UUIDType(binary=False).process_bind_param(
-        incident_id, db_session.bind.dialect
-    )
 
     enrichment_bl.enrich_entity(
-        fingerprint=fingerprint,
+        fingerprint=incident_id,
         enrichments=enrichment.enrichments,
         action_type=ActionType.INCIDENT_ENRICH,
         action_callee=authenticated_entity.email,
@@ -1137,6 +1138,7 @@ async def unenrich_incident(
         action_callee=authenticated_entity.email,
         action_description=f"Incident un-enriched by {authenticated_entity.email}",
         force=True,
+        entity_type="incident",
     )
 
     # Notify clients if pusher is available
diff --git a/keep/contextmanager/contextmanager.py b/keep/contextmanager/contextmanager.py
index 12ef27274d..590f89d228 100644
--- a/keep/contextmanager/contextmanager.py
+++ b/keep/contextmanager/contextmanager.py
@@ -9,6 +9,7 @@
 from keep.api.core.config import config
 from keep.api.core.db import get_last_workflow_execution_by_workflow_id, get_session
 from keep.api.models.alert import AlertDto
+from keep.api.models.incident import IncidentDto
 
 
 class ForeachContext(TypedDict):
@@ -33,7 +34,7 @@ def __init__(
         self.providers_context = {}
         self.actions_context = {}
         self.event_context: AlertDto = {}
-        self.incident_context = {}
+        self.incident_context: IncidentDto | None = None
         self.foreach_context: ForeachContext = {
             "value": None,
             "items": None,
diff --git a/keep/providers/base/base_provider.py b/keep/providers/base/base_provider.py
index edf8b2c1ed..8d9b54f449 100644
--- a/keep/providers/base/base_provider.py
+++ b/keep/providers/base/base_provider.py
@@ -206,6 +206,7 @@ def _enrich(self, enrichments, results, audit_enabled=True):
         """
         self.logger.debug("Extracting the fingerprint from the alert")
         event = None
+        entity_type: Literal["alert", "incident"] = "alert"
         if "fingerprint" in results:
             fingerprint = results["fingerprint"]
         elif self.context_manager.foreach_context.get("value", {}):
@@ -242,7 +243,8 @@ def _enrich(self, enrichments, results, audit_enabled=True):
             else:
                 fingerprint = self.context_manager.event_context.fingerprint
         elif self.context_manager.incident_context:
-            fingerprint = str(self.context_manager.incident_context.id)
+            entity_type = "incident"
+            fingerprint = self.context_manager.incident_context.id
         else:
             fingerprint = None
 
@@ -329,15 +331,16 @@ def convert_dot_to_bracket(match):
                 # enrich the alert with _enrichments
                 enrichments_bl.enrich_entity(
                     enrichments=_enrichments,
-                    action_description=f"Workflow enriched the alert with {enrichment_string}",
+                    action_description=f"Workflow enriched the {entity_type} with {enrichment_string}",
                     **common_kwargs,
                 )
 
-            if disposable_enrichments:
+            # todo: incidents do not have disposable enrichments
+            if disposable_enrichments and entity_type == "alert":
                 # enrich with disposable enrichments
                 enrichments_bl.disposable_enrich_entity(
                     enrichments=disposable_enrichments,
-                    action_description=f"Workflow enriched the alert with {disposable_enrichment_string}",
+                    action_description=f"Workflow enriched the {entity_type} with {disposable_enrichment_string}",
                     **common_kwargs,
                 )
 
@@ -351,11 +354,13 @@ def convert_dot_to_bracket(match):
 
         except Exception as e:
             self.logger.error(
-                "Failed to enrich alert in db",
+                f"Failed to enrich {entity_type} in db",
                 extra={"fingerprint": fingerprint, "provider": self.provider_id},
             )
             raise e
-        self.logger.info("Alert enriched", extra={"fingerprint": fingerprint})
+        self.logger.info(
+            f"{entity_type.capitalize()} enriched", extra={"fingerprint": fingerprint}
+        )
 
     def _notify(self, **kwargs):
         """
diff --git a/keep/providers/console_provider/console_provider.py b/keep/providers/console_provider/console_provider.py
index 6d12afaaee..6cea20e4e3 100644
--- a/keep/providers/console_provider/console_provider.py
+++ b/keep/providers/console_provider/console_provider.py
@@ -26,10 +26,23 @@ def dispose(self):
         # No need to dispose of anything, so just do nothing.
         pass
 
-    def _query(self, message: str = "", logger: bool = False, severity: str = "info"):
+    def _query(
+        self,
+        message: str = "",
+        logger: bool = False,
+        severity: str = "info",
+        **kwargs,  # TODO: remove '**kwargs', when we will pop it from the notify method in the base provider
+    ):
         return self._notify(message, logger, severity)
 
-    def _notify(self, message: str = "", logger: bool = False, severity: str = "info"):
+    def _notify(
+        self,
+        message: str = "",
+        logger: bool = False,
+        severity: str = "info",
+        **kwargs,
+        # TODO: remove '**kwargs', when we will pop it from the notify method in the base provider
+    ):
         """
         Output alert message simply using the print method.
 
diff --git a/keep/workflowmanager/workflowmanager.py b/keep/workflowmanager/workflowmanager.py
index 4d18f18cfc..1ff2f2a00a 100644
--- a/keep/workflowmanager/workflowmanager.py
+++ b/keep/workflowmanager/workflowmanager.py
@@ -27,11 +27,17 @@
 class WorkflowManager:
     # List of providers that are not allowed to be used in workflows in multi tenant mode.
     PREMIUM_PROVIDERS = ["bash", "python", "llamacpp", "ollama"]
+    _lock = threading.Lock()
+    _instance: typing.Optional["WorkflowManager"] = None
 
     @staticmethod
     def get_instance() -> "WorkflowManager":
-        if not hasattr(WorkflowManager, "_instance"):
-            WorkflowManager._instance = WorkflowManager()
+        if not WorkflowManager._instance:
+            # We don't want to lock if the instance is already created
+            with WorkflowManager._lock:
+                # Another thread might have created the instance while we were waiting for the lock
+                if not WorkflowManager._instance:
+                    WorkflowManager._instance = WorkflowManager()
         return WorkflowManager._instance
 
     def __init__(self):
@@ -67,6 +73,7 @@ def stop(self):
         self.started = False
         # Clear the scheduler reference
         self.scheduler = None
+        WorkflowManager._instance = None
 
     def _apply_filter(self, filter_val, value):
         # if it's a regex, apply it
@@ -407,17 +414,19 @@ def insert_events(self, tenant_id, events: typing.List[AlertDto | IncidentDto]):
 
                         compiled_ast = self.cel_environment.compile(cel)
                         program = self.cel_environment.program(compiled_ast)
-                        
+
                         # Convert event to dict and normalize severity for CEL evaluation
                         event_payload = event.dict()
                         # Convert severity string to numeric order for proper comparison with preprocessed CEL
                         if isinstance(event_payload.get("severity"), str):
                             try:
-                                event_payload["severity"] = AlertSeverity(event_payload["severity"].lower()).order
+                                event_payload["severity"] = AlertSeverity(
+                                    event_payload["severity"].lower()
+                                ).order
                             except (ValueError, AttributeError):
                                 # If severity conversion fails, keep original value
                                 pass
-                        
+
                         activation = celpy.json_to_cel(event_payload)
                         try:
                             should_run = program.evaluate(activation)
diff --git a/pyproject.toml b/pyproject.toml
index eae9fd2449..e6119f91b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -184,3 +184,8 @@ env = "GH_TOKEN"
 [tool.semantic_release.publish]
 dist_glob_patterns = ["dist/*"]
 upload_to_vcs_release = true
+
+[tool.pytest.ini_options]
+filterwarnings = [
+    "ignore::DeprecationWarning"
+]
\ No newline at end of file
diff --git a/tests/test_enrichments.py b/tests/test_enrichments.py
index c4a37a08d3..69bf89ca34 100644
--- a/tests/test_enrichments.py
+++ b/tests/test_enrichments.py
@@ -2,6 +2,7 @@
 import time
 from datetime import datetime
 from unittest.mock import MagicMock, Mock, patch
+import uuid
 
 import pytest
 from sqlalchemy import text
@@ -15,7 +16,10 @@
 from keep.api.models.db.extraction import ExtractionRule
 from keep.api.models.db.mapping import MappingRule
 from keep.api.models.db.topology import TopologyService
-from tests.fixtures.client import client, setup_api_key, test_app  # noqa
+from keep.api.models.db.workflow import Workflow
+from keep.workflowmanager.workflowmanager import WorkflowManager
+from tests.fixtures.client import client, setup_api_key, test_app
+from tests.fixtures.workflow_manager import wait_for_workflow_execution  # noqa
 
 
 @pytest.fixture(autouse=True)
@@ -900,3 +904,224 @@ def test_batch_enrichment(db_session, client, test_app, create_alert, elastic_cl
 
     assert len(alerts) == 10
     assert [a["status"] for a in alerts] == ["acknowledged"] * 10
+
+
+@pytest.mark.parametrize("test_app", ["NO_AUTH"], indirect=True)
+def test_incident_manual_enrichment_integration(db_session, client, test_app):
+    """
+    Test scenario 1: Create incident via API → enrich it manually → fetch and check enrichment
+    """
+    # Create incident via API
+    incident_payload = {
+        "user_generated_name": "Test Incident for Manual Enrichment",
+        "user_summary": "Test incident for manual enrichment integration test",
+        "severity": "critical",
+        "status": "firing",
+    }
+
+    # Create the incident
+    response = client.post(
+        "/incidents",
+        headers={"x-api-key": "some-key"},
+        json=incident_payload,
+    )
+    assert response.status_code == 202
+    incident_data = response.json()
+    incident_id = incident_data["id"]
+
+    # Enrich the incident manually with jira_ticket field
+    enrichment_payload = {"enrichments": {"jira_ticket": "12345"}}
+
+    response = client.post(
+        f"/incidents/{incident_id}/enrich",
+        headers={"x-api-key": "some-key"},
+        json=enrichment_payload,
+    )
+    assert response.status_code == 202
+
+    # Fetch the incident and check the enrichment is there
+    response = client.get(
+        f"/incidents/{incident_id}",
+        headers={"x-api-key": "some-key"},
+    )
+    assert response.status_code == 200
+    incident_data = response.json()
+
+    # Verify the enrichment was applied
+    assert "enrichments" in incident_data
+    assert incident_data["enrichments"]["jira_ticket"] == "12345"
+
+
+@pytest.mark.parametrize(
+    "test_app, db_session",
+    [
+        ("NO_AUTH", None),
+        ("NO_AUTH", {"db": "mysql"}),
+    ],
+    indirect=True,
+)
+def test_incident_workflow_enrichment_integration(db_session, client, test_app):
+    """
+    Test scenario 2: Create workflow that enriches incidents → create incident → fetch and check enrichment
+    """
+
+    # Create a workflow that enriches every incident with jira_ticket field
+    workflow_definition = """workflow:
+  id: incident-jira-enricher-test
+  name: Incident JIRA Enricher Test
+  description: Test workflow that enriches incidents with JIRA ticket
+  disabled: false
+  triggers:
+    - type: incident
+      events:
+        - created
+  actions:
+    - name: enrich-with-jira
+      provider:
+        type: console
+        with:
+          message: "Enriching incident {{ incident.user_generated_name }} with JIRA ticket"
+          enrich_incident:
+            - key: jira_ticket
+              value: "12345"
+"""
+
+    # Add the workflow to the database
+    workflow = Workflow(
+        id="incident-jira-enricher-test",
+        name="Incident JIRA Enricher Test",
+        tenant_id=SINGLE_TENANT_UUID,
+        description="Test workflow that enriches incidents with JIRA ticket",
+        created_by="test@keephq.dev",
+        interval=0,
+        workflow_raw=workflow_definition,
+        last_updated=datetime.utcnow(),
+    )
+    db_session.add(workflow)
+    db_session.commit()
+
+    # Create incident via API
+    incident_payload = {
+        "user_generated_name": "Test Incident for Workflow Enrichment",
+        "user_summary": "Test incident for workflow enrichment integration test",
+        "severity": "critical",
+        "status": "firing",
+    }
+
+    response = client.post(
+        "/incidents",
+        headers={"x-api-key": "some-key"},
+        json=incident_payload,
+    )
+    assert response.status_code == 202
+    incident_data = response.json()
+    incident_id = incident_data["id"]
+
+    # wait a bit, to be sure workflow is added to the queue
+    workflow_manager = WorkflowManager.get_instance()
+    assert len(workflow_manager.scheduler.workflows_to_run) == 1
+
+    # Wait for workflow execution to complete
+    workflow_execution = wait_for_workflow_execution(
+        SINGLE_TENANT_UUID, "incident-jira-enricher-test"
+    )
+
+    # Verify workflow execution was successful
+    assert workflow_execution is not None
+    assert workflow_execution.status == "success"
+
+    # Fetch the incident and check the enrichment is there
+    response = client.get(
+        f"/incidents/{incident_id}",
+        headers={"x-api-key": "some-key"},
+    )
+    assert response.status_code == 200
+    incident_data = response.json()
+
+    # Verify the enrichment was applied by the workflow
+    assert "enrichments" in incident_data
+    assert incident_data["enrichments"]["jira_ticket"] == "12345"
+
+
+@pytest.mark.parametrize(
+    "test_app, db_session",
+    [
+        ("NO_AUTH", None),
+        ("NO_AUTH", {"db": "mysql"}),
+    ],
+    indirect=True,
+)
+def test_alert_enrichment_via_api_uuid(db_session, client, test_app, create_alert):
+    fingerprint = str(uuid.uuid4())
+
+    create_alert(
+        fingerprint,
+        AlertStatus.FIRING,
+        datetime.utcnow(),
+        {},
+    )
+
+    enrichment_response = client.post(
+        "/alerts/enrich",
+        headers={"x-api-key": "some-key"},
+        json={
+            "fingerprint": fingerprint,
+            "enrichments": {
+                "jira_ticket": "12345",
+            },
+        },
+    )
+
+    assert enrichment_response.status_code == 200
+
+    alert_response = client.get(
+        f"/alerts/{fingerprint}",
+        headers={"x-api-key": "some-key"},
+    )
+    assert alert_response.status_code == 200
+    alert_data = alert_response.json()
+
+    assert alert_data["enriched_fields"] == ["jira_ticket"]
+    assert alert_data["jira_ticket"] == "12345"
+
+
+@pytest.mark.parametrize(
+    "test_app, db_session",
+    [
+        ("NO_AUTH", None),
+        ("NO_AUTH", {"db": "mysql"}),
+    ],
+    indirect=True,
+)
+def test_alert_enrichment_via_api_non_uuid(db_session, client, test_app, create_alert):
+    not_uuid_fingerprint = "not-uuid-fingerprint"
+
+    create_alert(
+        not_uuid_fingerprint,
+        AlertStatus.FIRING,
+        datetime.utcnow(),
+        {},
+    )
+
+    enrichment_response = client.post(
+        "/alerts/enrich",
+        headers={"x-api-key": "some-key"},
+        json={
+            "fingerprint": "not-uuid-fingerprint",
+            "enrichments": {
+                "jira_ticket": "12345",
+            },
+        },
+    )
+
+    assert enrichment_response.status_code == 200
+
+    alert_response = client.get(
+        f"/alerts/{not_uuid_fingerprint}",
+        headers={"x-api-key": "some-key"},
+    )
+    assert alert_response.status_code == 200
+    alert_data = alert_response.json()
+
+    assert alert_data["enriched_fields"] == ["jira_ticket"]
+    assert alert_data["jira_ticket"] == "12345"


Code review comments:
diff_hunk: @@ -67,6 +73,7 @@ def stop(self):
         self.started = False
         # Clear the scheduler reference
         self.scheduler = None
+        WorkflowManager._instance = None
path: keep/workflowmanager/workflowmanager.py
text: @user1:
logic: Setting _instance to None could cause issues if other threads are still holding references. Consider adding a check to ensure no workflows are in progress before nullifying

diff_hunk: @@ -0,0 +1,45 @@
+"""Remove dashes from alert_fingerprint UUIDs for mysql and sqlite
+
+Revision ID: 613f4a584f37
+Revises: 9dd1be4539e0
+Create Date: 2025-07-01 12:19:08.895646
+
+We look up incident enrichments using cast(alert_fingerprint as char). MySQL casts uuid to varchar without dashes. However, until now we used Python's `str()` to cast the uuid to a string when creating the incident enrichment, resulting in enrichments not found. This migration removes the dashes from previous entries.
+
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "613f4a584f37"
+down_revision = "9dd1be4539e0"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    dialect = op.get_context().dialect.name
+    if dialect == "postgresql":
+        pass
+    elif dialect == "mysql":
+        # for mysql find all uuid strings and replace the dashes with empty strings, as mysql casts uuid to varchar without dashes,
+        op.execute(
+            """UPDATE IGNORE alertenrichment 
+            SET alert_fingerprint = REPLACE(alert_fingerprint, '-', '')
+            WHERE CHAR_LENGTH(alert_fingerprint) = 36 
+            AND alert_fingerprint REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
+            """
path: keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py
text: @user1:
logic: IGNORE clause could silently skip rows with conflicts. Consider using a transaction and logging skipped rows for verification.

diff_hunk: @@ -0,0 +1,45 @@
+"""Remove dashes from alert_fingerprint UUIDs for mysql and sqlite
+
+Revision ID: 613f4a584f37
+Revises: 9dd1be4539e0
+Create Date: 2025-07-01 12:19:08.895646
+
+We look up incident enrichments using cast(alert_fingerprint as char). MySQL casts uuid to varchar without dashes. However, until now we used Python's `str()` to cast the uuid to a string when creating the incident enrichment, resulting in enrichments not found. This migration removes the dashes from previous entries.
+
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "613f4a584f37"
+down_revision = "9dd1be4539e0"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    dialect = op.get_context().dialect.name
+    if dialect == "postgresql":
+        pass
+    elif dialect == "mysql":
+        # for mysql find all uuid strings and replace the dashes with empty strings, as mysql casts uuid to varchar without dashes,
+        op.execute(
+            """UPDATE IGNORE alertenrichment 
+            SET alert_fingerprint = REPLACE(alert_fingerprint, '-', '')
+            WHERE CHAR_LENGTH(alert_fingerprint) = 36 
+            AND alert_fingerprint REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
+            """
+        )
+    elif dialect == "sqlite":
+        op.execute(
+            """UPDATE or IGNORE alertenrichment 
+            SET alert_fingerprint = REPLACE(alert_fingerprint, '-', '')
+            WHERE LENGTH(alert_fingerprint) = 36 
+            AND alert_fingerprint REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
+            """
+        )
+    pass
+
+
+def downgrade() -> None:
+    pass
path: keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py
text: @user1:
logic: Empty downgrade() means this migration is not reversible. Implementation needed for safely reverting changes.

```suggestion
def downgrade() -> None:
    dialect = op.get_context().dialect.name
    if dialect == "mysql":
        op.execute(
            """UPDATE alertenrichment 
            SET alert_fingerprint = CONCAT(
                SUBSTRING(alert_fingerprint, 1, 8), '-',
                SUBSTRING(alert_fingerprint, 9, 4), '-',
                SUBSTRING(alert_fingerprint, 13, 4), '-',
                SUBSTRING(alert_fingerprint, 17, 4), '-',
                SUBSTRING(alert_fingerprint, 21)
            )
            WHERE CHAR_LENGTH(alert_fingerprint) = 32;"""
        )
    elif dialect == "sqlite":
        op.execute(
            """UPDATE alertenrichment 
            SET alert_fingerprint = substr(alert_fingerprint, 1, 8) || '-' ||
                substr(alert_fingerprint, 9, 4) || '-' ||
                substr(alert_fingerprint, 13, 4) || '-' ||
                substr(alert_fingerprint, 17, 4) || '-' ||
                substr(alert_fingerprint, 21)
            WHERE LENGTH(alert_fingerprint) = 32;"""
        )
```

diff_hunk: @@ -0,0 +1,45 @@
+"""Remove dashes from alert_fingerprint UUIDs for mysql and sqlite
+
+Revision ID: 613f4a584f37
+Revises: 9dd1be4539e0
+Create Date: 2025-07-01 12:19:08.895646
+
+We look up incident enrichments using cast(alert_fingerprint as char). MySQL casts uuid to varchar without dashes. However, until now we used Python's `str()` to cast the uuid to a string when creating the incident enrichment, resulting in enrichments not found. This migration removes the dashes from previous entries.
+
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "613f4a584f37"
+down_revision = "9dd1be4539e0"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    dialect = op.get_context().dialect.name
+    if dialect == "postgresql":
+        pass
path: keep/api/models/db/migrations/versions/2025-07-01-12-19_alertenrichment_uuid_dashes.py
text: @user1:
logic: Empty pass for PostgreSQL could lead to inconsistent data. Either document why PostgreSQL doesn't need changes or implement the same standardization.

diff_hunk: @@ -900,3 +904,224 @@ def test_batch_enrichment(db_session, client, test_app, create_alert, elastic_cl
 
     assert len(alerts) == 10
     assert [a["status"] for a in alerts] == ["acknowledged"] * 10
+
+
+@pytest.mark.parametrize("test_app", ["NO_AUTH"], indirect=True)
+def test_incident_manual_enrichment_integration(db_session, client, test_app):
+    """
+    Test scenario 1: Create incident via API → enrich it manually → fetch and check enrichment
+    """
+    # Create incident via API
+    incident_payload = {
+        "user_generated_name": "Test Incident for Manual Enrichment",
+        "user_summary": "Test incident for manual enrichment integration test",
+        "severity": "critical",
+        "status": "firing",
+    }
+
+    # Create the incident
+    response = client.post(
+        "/incidents",
+        headers={"x-api-key": "some-key"},
+        json=incident_payload,
+    )
+    assert response.status_code == 202
+    incident_data = response.json()
+    incident_id = incident_data["id"]
+
+    # Enrich the incident manually with jira_ticket field
+    enrichment_payload = {"enrichments": {"jira_ticket": "12345"}}
+
+    response = client.post(
+        f"/incidents/{incident_id}/enrich",
+        headers={"x-api-key": "some-key"},
+        json=enrichment_payload,
+    )
+    assert response.status_code == 202
+
+    # Fetch the incident and check the enrichment is there
+    response = client.get(
+        f"/incidents/{incident_id}",
+        headers={"x-api-key": "some-key"},
+    )
+    assert response.status_code == 200
+    incident_data = response.json()
+
+    # Verify the enrichment was applied
+    assert "enrichments" in incident_data
+    assert incident_data["enrichments"]["jira_ticket"] == "12345"
+
+
+@pytest.mark.parametrize(
+    "test_app, db_session",
+    [
+        ("NO_AUTH", None),
+        ("NO_AUTH", {"db": "mysql"}),
+    ],
+    indirect=True,
+)
+def test_incident_workflow_enrichment_integration(db_session, client, test_app):
+    """
+    Test scenario 2: Create workflow that enriches incidents → create incident → fetch and check enrichment
+    """
+
+    # Create a workflow that enriches every incident with jira_ticket field
+    workflow_definition = """workflow:
+  id: incident-jira-enricher-test
+  name: Incident JIRA Enricher Test
+  description: Test workflow that enriches incidents with JIRA ticket
+  disabled: false
+  triggers:
+    - type: incident
+      events:
+        - created
+  actions:
+    - name: enrich-with-jira
+      provider:
+        type: console
+        with:
+          message: "Enriching incident {{ incident.user_generated_name }} with JIRA ticket"
+          enrich_incident:
+            - key: jira_ticket
+              value: "12345"
+"""
+
+    # Add the workflow to the database
+    workflow = Workflow(
+        id="incident-jira-enricher-test",
+        name="Incident JIRA Enricher Test",
+        tenant_id=SINGLE_TENANT_UUID,
+        description="Test workflow that enriches incidents with JIRA ticket",
+        created_by="test@keephq.dev",
+        interval=0,
+        workflow_raw=workflow_definition,
+        last_updated=datetime.utcnow(),
+    )
+    db_session.add(workflow)
+    db_session.commit()
+
+    # Create incident via API
+    incident_payload = {
+        "user_generated_name": "Test Incident for Workflow Enrichment",
+        "user_summary": "Test incident for workflow enrichment integration test",
+        "severity": "critical",
+        "status": "firing",
+    }
+
+    response = client.post(
+        "/incidents",
+        headers={"x-api-key": "some-key"},
+        json=incident_payload,
+    )
+    assert response.status_code == 202
+    incident_data = response.json()
+    incident_id = incident_data["id"]
+
+    # wait a bit, to be sure workflow is added to the queue
+    workflow_manager = WorkflowManager.get_instance()
+    assert len(workflow_manager.scheduler.workflows_to_run) == 1
path: tests/test_enrichments.py
text: @user1:
logic: Race condition risk. Checking queue length immediately after incident creation may be unreliable. Consider adding retry logic or a more robust wait mechanism.