Repository: keephq/keep
Pull request #3955: fix: workflow ui removes conditions and foreach
Author: login: Kiryous
Labels: name: Bug
description: Something isn't working
name: UI
description: User interface related issues
name: size:L
description: This PR changes 100-499 lines, ignoring generated files.
Created: 2025-03-10T16:28:21Z | Merged: 2025-03-10T18:54:59Z

<!-- 
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 #3953 

Linked issue #3953: [🐛 Bug]: Workflow UI Builder removes conditions

Diff:
diff --git a/keep-ui/entities/workflows/lib/__tests__/parser.test.ts b/keep-ui/entities/workflows/lib/__tests__/parser.test.ts
index 67adc5779a..0bcebcafec 100644
--- a/keep-ui/entities/workflows/lib/__tests__/parser.test.ts
+++ b/keep-ui/entities/workflows/lib/__tests__/parser.test.ts
@@ -299,19 +299,57 @@ workflow:
             },
           },
           {
-            id: "action-1",
-            name: "ntfy-action",
-            type: "action-ntfy",
-            componentType: "task" as const,
+            type: "condition-threshold",
+            componentType: "switch",
+            id: "a819c748-06ff-42cb-b3bc-e63732ae6b40",
             properties: {
-              config: "ntfy",
-              with: {
-                message: "Test message",
-                topic: "test",
-              },
-              stepParams: [],
-              actionParams: ["message", "topic"],
+              value: "{{ steps.clickhouse-step.results }}",
+              compare_to: "90%",
+            },
+            name: "threshold-condition",
+            branches: {
+              true: [
+                {
+                  id: "action-1",
+                  name: "ntfy-action",
+                  type: "action-ntfy",
+                  componentType: "task" as const,
+                  properties: {
+                    config: "ntfy",
+                    with: {
+                      message: "Test message",
+                      topic: "test",
+                    },
+                    stepParams: [],
+                    actionParams: ["message", "topic"],
+                  },
+                },
+              ],
+              false: [],
+            },
+          },
+          {
+            id: "foreach-1",
+            name: "Foreach",
+            type: "foreach",
+            componentType: "container" as const,
+            properties: {
+              value: "{{ steps.clickhouse-step.results.items }}",
             },
+            sequence: [
+              {
+                id: "console-step",
+                name: "Console",
+                type: "step-console",
+                componentType: "task" as const,
+                properties: {
+                  with: {
+                    message: "{{ item }}",
+                  },
+                  stepParams: ["message"],
+                },
+              },
+            ],
           },
         ],
         properties: {
@@ -330,10 +368,16 @@ workflow:
 
       expect(result.id).toBe("test-workflow");
       expect(result.name).toBe("Test Workflow");
-      expect(result.steps).toHaveLength(1);
+      expect(result.steps).toHaveLength(2);
       expect(result.actions).toHaveLength(1);
       expect(result.steps[0].name).toBe("clickhouse-step");
       expect(result.actions![0].name).toBe("ntfy-action");
+      expect(result.actions![0].condition).toHaveLength(1);
+      expect(result.actions![0].condition![0].type).toBe("threshold");
+      expect(result.steps[1].name).toBe("Console");
+      expect(result.steps[1].foreach).toBe(
+        "{{ steps.clickhouse-step.results.items }}"
+      );
     });
 
     it("should handle workflow with conditions and foreach", () => {
diff --git a/keep-ui/entities/workflows/lib/parser.ts b/keep-ui/entities/workflows/lib/parser.ts
index 368c542fae..de467cd48c 100644
--- a/keep-ui/entities/workflows/lib/parser.ts
+++ b/keep-ui/entities/workflows/lib/parser.ts
@@ -281,45 +281,61 @@ export function getWithParams(
   return withParams;
 }
 
+function getYamlConditionFromStep(
+  condition: V2StepConditionThreshold | V2StepConditionAssert
+): YamlThresholdCondition | YamlAssertCondition {
+  return condition.type === "condition-threshold"
+    ? {
+        name: condition.name,
+        type: "threshold" as const,
+        value: condition.properties.value,
+        compare_to: condition.properties.compare_to,
+      }
+    : {
+        name: condition.name,
+        type: "assert" as const,
+        assert: condition.properties.assert,
+      };
+}
+
 function getActionsFromCondition(
   condition: V2StepConditionThreshold | V2StepConditionAssert,
   foreach?: string
-): YamlStepOrAction[] {
-  const compiledCondition =
-    condition.type === "condition-threshold"
-      ? {
-          name: condition.name,
-          type: "threshold" as const,
-          value: condition.properties.value,
-          compare_to: condition.properties.compare_to,
-        }
-      : {
-          name: condition.name,
-          type: "assert" as const,
-          assert: condition.properties.assert,
-        };
+): { actions: YamlStepOrAction[]; steps: YamlStepOrAction[] } {
   const steps: (V2StepStep | V2ActionStep)[] = condition?.branches?.true || [];
-  const compiledActions = steps.map((a) => {
+  const compiledActions: YamlStepOrAction[] = [];
+  const compiledSteps: YamlStepOrAction[] = [];
+  steps.forEach((a) => {
     if (a.type.startsWith("step-")) {
-      const compiledAction = getYamlStepFromStep(a as V2StepStep);
-      if (foreach) {
-        compiledAction["foreach"] = foreach;
-        compiledAction["condition"] = [compiledCondition];
-      }
-      return compiledAction;
+      const compiledAction = getYamlStepFromStep(a as V2StepStep, {
+        condition,
+        foreach,
+      });
+      compiledSteps.push(compiledAction);
     } else {
-      const compiledAction = getYamlActionFromAction(a as V2ActionStep);
-      if (foreach) {
-        compiledAction["foreach"] = foreach;
-        compiledAction["condition"] = [compiledCondition];
-      }
-      return compiledAction;
+      const compiledAction = getYamlActionFromAction(a as V2ActionStep, {
+        condition,
+        foreach,
+      });
+      compiledActions.push(compiledAction);
     }
   });
-  return compiledActions;
+  return {
+    actions: compiledActions,
+    steps: compiledSteps,
+  };
 }
 
-export function getYamlStepFromStep(s: V2StepStep): YamlStepOrAction {
+export function getYamlStepFromStep(
+  s: V2StepStep,
+  {
+    condition,
+    foreach,
+  }: {
+    condition?: V2StepConditionThreshold | V2StepConditionAssert;
+    foreach?: string;
+  } = {}
+): YamlStepOrAction {
   const withParams = getWithParams(s);
   const providerType = s.type.replace("step-", "");
   const providerName =
@@ -336,6 +352,8 @@ export function getYamlStepFromStep(s: V2StepStep): YamlStepOrAction {
   const step: YamlStepOrAction = {
     name: s.name,
     if: ifParam,
+    condition: condition ? [getYamlConditionFromStep(condition)] : undefined,
+    foreach: foreach ? foreach : undefined,
     provider: provider,
   };
   if (s.properties.vars) {
@@ -344,7 +362,16 @@ export function getYamlStepFromStep(s: V2StepStep): YamlStepOrAction {
   return step;
 }
 
-export function getYamlActionFromAction(s: V2ActionStep): YamlStepOrAction {
+export function getYamlActionFromAction(
+  s: V2ActionStep,
+  {
+    condition,
+    foreach,
+  }: {
+    condition?: V2StepConditionThreshold | V2StepConditionAssert;
+    foreach?: string;
+  } = {}
+): YamlStepOrAction {
   const withParams = getWithParams(s);
   const providerType = s.type.replace("action-", "");
   const providerName =
@@ -361,6 +388,8 @@ export function getYamlActionFromAction(s: V2ActionStep): YamlStepOrAction {
   const action: YamlStepOrAction = {
     name: s.name,
     if: ifParam,
+    condition: condition ? [getYamlConditionFromStep(condition)] : undefined,
+    foreach: foreach ? foreach : undefined,
     provider: provider,
   };
   if (s.properties.vars) {
@@ -384,7 +413,7 @@ export function getYamlWorkflowDefinition(
   const services = (alert.properties.services as string[]) ?? [];
   const consts = (alert.properties.consts as Record<string, string>) ?? {};
   // Steps (move to func?)
-  const steps = alert.sequence
+  let steps = alert.sequence
     .filter((s): s is V2StepStep => s.type.startsWith("step-"))
     .map((s: V2StepStep) => getYamlStepFromStep(s));
   // Actions
@@ -394,57 +423,49 @@ export function getYamlWorkflowDefinition(
   // Actions > Foreach
   alert.sequence
     .filter((step): step is V2StepForeach => step.type === "foreach")
-    ?.forEach((forEach: V2StepForeach) => {
-      const forEachValue = forEach?.properties?.value as string;
-      // FIX: type
-      const condition = forEach?.sequence?.find((c) =>
-        c.type.startsWith("condition-")
-      ) as unknown as V2StepConditionAssert | V2StepConditionThreshold;
-      let foreachActions = [] as YamlStepOrAction[];
+    .forEach((forEach: V2StepForeach) => {
+      const forEachValue = forEach.properties.value as string;
+      const condition = forEach.sequence.find(
+        (step): step is V2StepConditionThreshold | V2StepConditionAssert =>
+          step.type === "condition-assert" ||
+          step.type === "condition-threshold"
+      );
       if (condition) {
-        foreachActions = getActionsFromCondition(condition, forEachValue);
+        const { actions: conditionActions, steps: conditionSteps } =
+          getActionsFromCondition(condition, forEachValue);
+        actions = [...actions, ...conditionActions];
+        steps = [...steps, ...conditionSteps];
       } else {
         const forEachSequence = forEach?.sequence || [];
         const stepOrAction = forEachSequence[0] as V2StepStep | V2ActionStep;
         if (!stepOrAction) {
           return;
         }
-        const withParams = getWithParams(stepOrAction);
-        const providerType = stepOrAction.type
-          .replace("action-", "")
-          .replace("step-", "");
-        const ifParam = stepOrAction.properties.if;
-        const providerName =
-          (stepOrAction.properties.config as string)?.trim() ||
-          `default-${providerType}`;
-        const provider = {
-          type: stepOrAction.type.replace("action-", "").replace("step-", ""),
-          config: `{{ providers.${providerName} }}`,
-          with: withParams,
-        };
-        const foreachAction: any = {
-          name: stepOrAction.name || "",
-          provider: provider,
-          foreach: forEachValue,
-        };
-        if (ifParam) {
-          foreachAction.if = ifParam as string;
-        }
-        if (stepOrAction.properties.vars) {
-          foreachAction.vars = stepOrAction.properties.vars;
+        if (stepOrAction.type === "action") {
+          actions.push(
+            getYamlActionFromAction(stepOrAction as V2ActionStep, {
+              foreach: forEachValue,
+            })
+          );
+        } else {
+          steps.push(
+            getYamlStepFromStep(stepOrAction as V2StepStep, {
+              foreach: forEachValue,
+            })
+          );
         }
-        foreachActions = [foreachAction];
       }
-      actions = [...actions, ...foreachActions];
     });
   // Actions > Condition
   alert.sequence
     .filter((step): step is V2StepConditionThreshold | V2StepConditionAssert =>
       step.type.startsWith("condition-")
     )
-    ?.forEach((condition) => {
-      const conditionActions = getActionsFromCondition(condition);
+    .forEach((condition) => {
+      const { actions: conditionActions, steps: conditionSteps } =
+        getActionsFromCondition(condition);
       actions = [...actions, ...conditionActions];
+      steps = [...steps, ...conditionSteps];
     });
 
   const triggers = [];


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR fixes workflow UI issues where conditions and foreach loops were being dropped during workflow editing by enhancing the parser's handling of nested workflow structures.

- Added proper handling of nested foreach/condition combinations in `getActionsFromCondition` function
- Updated `getYamlStepFromStep` and `getYamlActionFromAction` to preserve condition and foreach parameters
- Added comprehensive test coverage for complex workflow structures with nested conditions and foreach loops
- Fixed YAML serialization to maintain workflow structure integrity during UI edits

<sub>💡 (3/5) Reply to the bot's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>

<sub>2 file(s) reviewed, 2 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=keep_3955)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:30:01Z
user: login: Matvey-Kuk
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:50:47Z

Review comments:
diff_hunk: @@ -394,57 +425,48 @@ export function getYamlWorkflowDefinition(
   // Actions > Foreach
   alert.sequence
     .filter((step): step is V2StepForeach => step.type === "foreach")
-    ?.forEach((forEach: V2StepForeach) => {
+    .forEach((forEach: V2StepForeach) => {
       const forEachValue = forEach?.properties?.value as string;
       // FIX: type
       const condition = forEach?.sequence?.find((c) =>
         c.type.startsWith("condition-")
       ) as unknown as V2StepConditionAssert | V2StepConditionThreshold;
path: keep-ui/entities/workflows/lib/parser.ts
user: login: greptile-apps[bot]
body: logic: Unsafe type assertion. Consider adding runtime type checking before the cast to V2StepConditionAssert | V2StepConditionThreshold.
created_at: 2025-03-10T16:29:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -299,19 +299,57 @@ workflow:
             },
           },
           {
-            id: "action-1",
-            name: "ntfy-action",
-            type: "action-ntfy",
-            componentType: "task" as const,
+            type: "condition-threshold",
+            componentType: "switch",
+            id: "a819c748-06ff-42cb-b3bc-e63732ae6b40",
             properties: {
-              config: "ntfy",
-              with: {
-                message: "Test message",
-                topic: "test",
-              },
-              stepParams: [],
-              actionParams: ["message", "topic"],
+              value: "{{ steps.db-no-space.results }}",
+              compare_to: "90%",
path: keep-ui/entities/workflows/lib/__tests__/parser.test.ts
user: login: greptile-apps[bot]
body: logic: The step name 'db-no-space' doesn't match any previous step in the test. Should be 'clickhouse-step' for consistency.

```suggestion
              value: "{{ steps.clickhouse-step.results }}",
              compare_to: "90%",
```
created_at: 2025-03-10T16:29:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-10T16:28:25Z
body: [vc]: #YA1LXSOxW6KAOvlX+9IAS0EITRYR4w+TEz7Ri5lKQNI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJrZWVwIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2tlZXBocS9rZWVwLzVFNnVjaHdTeFJuWnpaZXc2QUJDNkpFQlFOWDIiLCJwcmV2aWV3VXJsIjoia2VlcC1naXQtZml4LTM5NTMtd2YtdWktY29uZGl0aW9ucy1rZWVwaHEudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJrZWVwLWdpdC1maXgtMzk1My13Zi11aS1jb25kaXRpb25zLWtlZXBocS52ZXJjZWwuYXBwIn0sInJvb3REaXJlY3RvcnkiOiJrZWVwLXVpIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **keep** | ✅ Ready ([Inspect](https://vercel.com/keephq/keep/5E6uchwSxRnZzZew6ABC6JEBQNX2)) | [Visit Preview](https://keep-git-fix-3953-wf-ui-conditions-keephq.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/keep-git-fix-3953-wf-ui-conditions-keephq.vercel.app?via=pr-comment-feedback-link) | Mar 10, 2025 6:36pm |


author_association: NONE
user: login: Kiryous
created_at: 2025-03-10T16:29:15Z
body: @greptileai
author_association: CONTRIBUTOR