Repository: keephq/keep

Issue:
[🐛 Bug]: CEL expression evaluation in workflows
**Describe the bug**

CEL expressions are not evaluating correctly in workflows.
For example we have a Slack forwarder from Prometheus alerts with this condition:

`
  triggers:
    - type: manual
    - type: alert
      cel: severity > 'info' && source.contains('prometheus')
`

and its only forwarding the 'warning' severity, a 'high' severity alert not triggering it.

I had a test with 'severity > 1', but it didnt worked also.


**Expected behavior**

The expected behaviour is to forward all of the alerts with bigger severity than 'info' and handle the CEL expression as in the UI (which is okay).




Discussion:
<!-- Answer -->
The reason `severity > 'info'` doesn't work as expected is that severity is treated as a string in CEL expressions, not as an ordinal or numeric value. String comparison doesn't reflect the intended order of severities (e.g., 'high' is not "greater than" 'info' in string sorting). The recommended approach is to use explicit matches, like `severity == 'warning' || severity == 'high'`, to filter for multiple severities ([docs](https://docs.keephq.dev/workflows/syntax/triggers), [overview](https://docs.keephq.dev/overview/cel)).

There are also known issues with type mismatches in CEL expressions (for example, fields like `source` may be lists or strings depending on the alert source), so always use `.contains()` for list fields and exact matches for string fields ([discussion](https://github.com/keephq/keep/discussions/5045), [issue](https://github.com/keephq/keep/issues/4777)).

If you need ordinal severity filtering, you would need to explicitly list all the severities you want to match. There is currently no built-in mapping of severity levels to numeric values for comparison ([docs](https://docs.keephq.dev/alerts/presets)).


<!-- 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/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a2ed37bb-b3a8-4581-9d16-b713f867e945?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/5086)



Patch:
diff --git a/keep/workflowmanager/workflowmanager.py b/keep/workflowmanager/workflowmanager.py
index de159958ad..4d18f18cfc 100644
--- a/keep/workflowmanager/workflowmanager.py
+++ b/keep/workflowmanager/workflowmanager.py
@@ -21,6 +21,7 @@
 from keep.workflowmanager.workflow import Workflow
 from keep.workflowmanager.workflowscheduler import WorkflowScheduler, timing_histogram
 from keep.workflowmanager.workflowstore import WorkflowStore
+from keep.api.utils.cel_utils import preprocess_cel_expression
 
 
 class WorkflowManager:
@@ -380,9 +381,44 @@ def insert_events(self, tenant_id, events: typing.List[AlertDto | IncidentDto]):
                                 )
                                 continue
 
+                        # Preprocess the CEL expression to handle severity comparisons properly
+                        try:
+                            cel = preprocess_cel_expression(cel)
+                            self.logger.debug(
+                                "Preprocessed CEL expression",
+                                extra={
+                                    "original_cel": trigger.get("cel", ""),
+                                    "preprocessed_cel": cel,
+                                    "workflow_id": workflow_model.id,
+                                    "tenant_id": tenant_id,
+                                },
+                            )
+                        except Exception:
+                            self.logger.exception(
+                                "Error preprocessing CEL expression",
+                                extra={
+                                    "cel": cel,
+                                    "trigger": trigger,
+                                    "workflow_id": workflow_model.id,
+                                    "tenant_id": tenant_id,
+                                },
+                            )
+                            continue
+
                         compiled_ast = self.cel_environment.compile(cel)
                         program = self.cel_environment.program(compiled_ast)
-                        activation = celpy.json_to_cel(event.dict())
+                        
+                        # 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
+                            except (ValueError, AttributeError):
+                                # If severity conversion fails, keep original value
+                                pass
+                        
+                        activation = celpy.json_to_cel(event_payload)
                         try:
                             should_run = program.evaluate(activation)
                         except celpy.evaluation.CELEvalError as e:


Test patch:
diff --git a/tests/test_workflow_severity_comparisons.py b/tests/test_workflow_severity_comparisons.py
new file mode 100644
index 0000000000..4dd82bce8a
--- /dev/null
+++ b/tests/test_workflow_severity_comparisons.py
@@ -0,0 +1,279 @@
+import datetime
+import pytest
+
+from keep.api.core.dependencies import SINGLE_TENANT_UUID
+from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus
+from keep.api.models.db.workflow import Workflow
+
+from tests.fixtures.workflow_manager import workflow_manager  # noqa
+
+
+@pytest.fixture
+def create_workflow(db_session):
+    """Fixture to create a workflow with a specific CEL expression"""
+
+    def _create_workflow(workflow_id, cel_expression):
+        workflow_definition = f"""workflow:
+  id: {workflow_id}
+  description: Test severity CEL expressions
+  triggers:
+    - type: alert
+      cel: {cel_expression}
+  actions:
+    - name: test-action
+      provider:
+        type: console
+        with:
+          message: "Alert matched CEL expression"
+"""
+        workflow = Workflow(
+            id=workflow_id,
+            name=workflow_id,
+            tenant_id=SINGLE_TENANT_UUID,
+            description="Test severity CEL expressions",
+            created_by="test@keephq.dev",
+            interval=0,
+            workflow_raw=workflow_definition,
+        )
+        db_session.add(workflow)
+        db_session.commit()
+        return workflow
+
+    return _create_workflow
+
+
+@pytest.fixture
+def create_alert():
+    """Fixture to create an alert DTO with specified properties"""
+
+    def _create_alert(**properties):
+        alert_data = {
+            "id": "test-alert-1",
+            "source": ["prometheus"],
+            "name": "test-alert",
+            "status": AlertStatus.FIRING,
+            "severity": AlertSeverity.INFO,
+            "lastReceived": datetime.datetime.now().isoformat(),
+            "fingerprint": f"test-fingerprint-{datetime.datetime.now().timestamp()}",
+        }
+        alert_data.update(properties)
+        return AlertDto(**alert_data)
+
+    return _create_alert
+
+
+def test_severity_greater_than_info_bug_fix(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """
+    Test the specific bug case from GitHub issue #5086:
+    severity > 'info' should match 'warning', 'high', and 'critical' severities
+    
+    Before fix: This would fail because 'high' < 'info' lexicographically (h < i)
+    After fix: This works because high (4) > info (2) numerically
+    """
+    # Create a workflow with the exact CEL expression from the bug report
+    workflow = create_workflow(
+        "test-severity-gt-info-bug", 
+        "severity > 'info' && source.contains('prometheus')"
+    )
+
+    # These alerts should match (severity > info)
+    high_alert = create_alert(
+        severity=AlertSeverity.HIGH, 
+        fingerprint="fp-high"
+    )
+    critical_alert = create_alert(
+        severity=AlertSeverity.CRITICAL, 
+        fingerprint="fp-critical"
+    )
+    warning_alert = create_alert(
+        severity=AlertSeverity.WARNING, 
+        fingerprint="fp-warning"
+    )
+
+    # These alerts should NOT match
+    info_alert = create_alert(
+        severity=AlertSeverity.INFO, 
+        fingerprint="fp-info"
+    )
+    low_alert = create_alert(
+        severity=AlertSeverity.LOW, 
+        fingerprint="fp-low"
+    )
+
+    # Test high severity alert (should match)
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [high_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+    assert workflow_manager.scheduler.workflows_to_run[-1]["workflow_id"] == workflow.id
+
+    # Test critical severity alert (should match)
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [critical_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+    assert workflow_manager.scheduler.workflows_to_run[-1]["workflow_id"] == workflow.id
+
+    # Test warning severity alert (should match)
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [warning_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+    assert workflow_manager.scheduler.workflows_to_run[-1]["workflow_id"] == workflow.id
+
+    # Test info severity alert (should NOT match)
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [info_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before
+
+    # Test low severity alert (should NOT match)
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [low_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before
+
+
+def test_severity_greater_than_or_equal_warning(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """Test severity >= 'warning' comparisons work correctly with numeric conversion"""
+    workflow = create_workflow("test-severity-gte-warning", "severity >= 'warning'")
+
+    # Should match: critical, high, warning
+    critical_alert = create_alert(severity=AlertSeverity.CRITICAL, fingerprint="fp-critical")
+    high_alert = create_alert(severity=AlertSeverity.HIGH, fingerprint="fp-high")
+    warning_alert = create_alert(severity=AlertSeverity.WARNING, fingerprint="fp-warning")
+
+    # Should NOT match: info, low
+    info_alert = create_alert(severity=AlertSeverity.INFO, fingerprint="fp-info")
+    low_alert = create_alert(severity=AlertSeverity.LOW, fingerprint="fp-low")
+
+    # Test matching severities
+    for alert in [critical_alert, high_alert, warning_alert]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+
+    # Test non-matching severities
+    for alert in [info_alert, low_alert]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before
+
+
+def test_severity_less_than_high(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """Test severity < 'high' comparisons work correctly with numeric conversion"""
+    workflow = create_workflow("test-severity-lt-high", "severity < 'high'")
+
+    # Should match: info, low, warning
+    info_alert = create_alert(severity=AlertSeverity.INFO, fingerprint="fp-info")
+    low_alert = create_alert(severity=AlertSeverity.LOW, fingerprint="fp-low")
+    warning_alert = create_alert(severity=AlertSeverity.WARNING, fingerprint="fp-warning")
+
+    # Should NOT match: high, critical
+    high_alert = create_alert(severity=AlertSeverity.HIGH, fingerprint="fp-high")
+    critical_alert = create_alert(severity=AlertSeverity.CRITICAL, fingerprint="fp-critical")
+
+    # Test matching severities
+    for alert in [info_alert, low_alert, warning_alert]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+
+    # Test non-matching severities  
+    for alert in [high_alert, critical_alert]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before
+
+
+def test_complex_severity_expressions(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """Test complex CEL expressions involving severity comparisons"""
+    workflow = create_workflow(
+        "test-complex-severity",
+        "(severity >= 'warning' && source.contains('prometheus')) || (severity == 'critical' && source.contains('grafana'))"
+    )
+
+    # Should match: prometheus with warning+, grafana with critical
+    prometheus_critical = create_alert(
+        severity=AlertSeverity.CRITICAL, source=["prometheus"], fingerprint="fp1"
+    )
+    prometheus_high = create_alert(
+        severity=AlertSeverity.HIGH, source=["prometheus"], fingerprint="fp2"
+    )
+    prometheus_warning = create_alert(
+        severity=AlertSeverity.WARNING, source=["prometheus"], fingerprint="fp3"
+    )
+    grafana_critical = create_alert(
+        severity=AlertSeverity.CRITICAL, source=["grafana"], fingerprint="fp4"
+    )
+
+    # Should NOT match: prometheus with info/low, grafana with non-critical
+    prometheus_info = create_alert(
+        severity=AlertSeverity.INFO, source=["prometheus"], fingerprint="fp5"
+    )
+    grafana_high = create_alert(
+        severity=AlertSeverity.HIGH, source=["grafana"], fingerprint="fp6"
+    )
+
+    # Test matching alerts
+    for alert in [prometheus_critical, prometheus_high, prometheus_warning, grafana_critical]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+
+    # Test non-matching alerts
+    for alert in [prometheus_info, grafana_high]:
+        workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+        workflow_manager.insert_events(SINGLE_TENANT_UUID, [alert])
+        assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before
+
+
+def test_case_insensitive_severity_comparisons(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """Test that severity comparisons are case-insensitive after preprocessing"""
+    workflow = create_workflow("test-severity-case", "severity > 'INFO'")
+
+    # Should match despite case difference in CEL expression
+    high_alert = create_alert(severity=AlertSeverity.HIGH, fingerprint="fp-high")
+    
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [high_alert])
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1
+    
+
+def test_severity_preprocessing_cel_utils_integration(
+    db_session, workflow_manager, create_workflow, create_alert
+):
+    """
+    Test that the cel_utils.preprocess_cel_expression function is properly integrated
+    into the workflow manager to fix the lexicographic comparison bug
+    """
+    
+    # This test specifically validates that lexicographic issues are resolved
+    # Before fix: 'high' < 'info' lexicographically (h comes before i in alphabet)
+    # After fix: high (4) > info (2) numerically
+    
+    workflow = create_workflow(
+        "test-preprocessing-integration", 
+        "severity > 'info'"
+    )
+
+    # Create a 'high' severity alert - this is the key test case
+    # that would fail with lexicographic comparison but should pass with numeric
+    high_alert = create_alert(
+        severity=AlertSeverity.HIGH,
+        source=["test"], 
+        fingerprint="fp-high-severity"
+    )
+
+    workflows_to_run_before = len(workflow_manager.scheduler.workflows_to_run)
+    workflow_manager.insert_events(SINGLE_TENANT_UUID, [high_alert])
+    
+    # This assertion would fail before the fix, but should pass after
+    assert len(workflow_manager.scheduler.workflows_to_run) == workflows_to_run_before + 1, \
+        "HIGH severity alert should match 'severity > info' expression after preprocessing fix"
+    assert workflow_manager.scheduler.workflows_to_run[-1]["workflow_id"] == workflow.id
\ No newline at end of file