Repository: keephq/keep
Pull request: fix: mustache yaml validation
<!-- 
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 #4974 

# Summary
- Enforces dot notation over bracket notation (e.g., `providers.Default Grafana` instead of `providers['Default Grafana']`)
- Adds support for `{{ . }}` syntax in foreach loops while preventing its use outside loops
- Improves provider validation with helpful suggestions for available providers of the same type
- Validates step variables and prevents access to unknown variables

# Screenshots
## Suggest installed providers of that type
<img width="993" alt="Screenshot 2025-06-04 at 14 32 20" src="https://github.com/user-attachments/assets/65ad2470-15f1-442a-a4c2-2605018f8b8b" />

## Forbid bracket notation
<img width="993" alt="Screenshot 2025-06-04 at 14 32 30" src="https://github.com/user-attachments/assets/a4a7b6bd-216e-4bf6-99c4-5051033e53ef" />

## Allow "{{ . }}" syntax in steps with foreach loops
<img width="999" alt="Screenshot 2025-06-04 at 14 32 59" src="https://github.com/user-attachments/assets/393c3136-cada-4a2c-b610-111fbb212056" />

## Forbid {{ foreach/value/. }} in steps without foreach
<img width="988" alt="Screenshot 2025-06-04 at 14 39 52" src="https://github.com/user-attachments/assets/87828b23-599c-4a99-b468-29b39ca2564d" />

## Validate step vars
<img width="986" alt="Screenshot 2025-06-04 at 14 44 25" src="https://github.com/user-attachments/assets/0c45c394-b1f2-48f1-b33e-d8b0e4e675aa" />

## Forbid unknown variables
<img width="1167" alt="Screenshot 2025-06-04 at 14 50 15" src="https://github.com/user-attachments/assets/41cc58e3-5305-4136-9e3e-e5e1dfd00c70" />


Problem statement:
[🐛 Bug]: mustache variable validation in YAML isn't complete
This should be valid:
- `{{ . }}` if used inside a step with foreach

These should be invalid:
- `{{ providers['Default Grafana'] }}`: should use dot notation `{{ providers.Default Grafana }}` (mustache doesn't support whitespaces in dot notation, but we do)
- `{{ steps.python-step.result[0][0] }}`: should use dot notation `{{ steps.python-step.result.0.0 }}`

Hints from issue discussion:
<!-- Answer -->
You are correct that mustache variable validation in YAML is not fully complete. The current validation logic is implemented in files like `validate-definition.ts` and `validate-yaml.ts`, which check for proper syntax and access rules for mustache variables in workflow definitions. These functions enforce dot notation and validate that variable parts are non-empty, and they include checks for step, secret, constant, and provider references. For example, they ensure that variables like `{{ steps.First Step.results }}` are valid and that you cannot access the results of the current or a future step. They also issue errors or warnings for missing or invalid parts in the variable path, such as empty segments or non-existent secrets or providers ([source](https://github.com/keephq/keep/blob/ba9c791d24ac8f935f2faf0eaaca67df2844c6b4/keep-ui/entities/workflows/lib/validate-yaml.ts)).

However, there are still gaps. The code contains TODO comments for incomplete validation logic, especially for alert and incident properties, and for validating the structure of step results. The validation does not currently enforce dot notation strictly in all cases, so variables like `{{ providers['Default Grafana'] }}` or `{{ steps.python-step.result[0][0] }}` may not always be flagged as invalid, even though they should be according to your requirements ([source](https://github.com/keephq/keep/blob/ba9c791d24ac8f935f2faf0eaaca67df2844c6b4/keep-ui/entities/workflows/lib/validate-yaml.ts)).

Tests exist for many scenarios, including foreach usage, step result access, and invalid variable paths, but there is no explicit enforcement or error for bracket notation or whitespace in dot notation. The validation logic is context-aware and provides real-time feedback in the YAML editor, but it does not yet fully cover all edge cases you mentioned ([source](https://github.com/keephq/keep/blob/ba9c791d24ac8f935f2faf0eaaca67df2844c6b4/keep-ui/entities/workflows/lib/__tests__/validate-yaml.test.ts)).

There are no existing issues, discussions, or recent changes in the repository that directly address these specific validation gaps. Documentation does not cover these details either.

In summary, the validation logic is robust for many cases but does not yet fully enforce dot notation or reject bracket notation and whitespace in variable paths as you described. The codebase contains TODOs for further improvements, and your examples highlight areas where stricter validation is still needed.


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

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/955398cd-2006-4de8-b0ee-25ebb02ed831?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/4974)

Commit message: fix: severity mapping

Patch to review:
diff --git a/keep-ui/entities/workflows/lib/__tests__/validate-mustache.test.ts b/keep-ui/entities/workflows/lib/__tests__/validate-mustache-ui-builder.test.ts
similarity index 56%
rename from keep-ui/entities/workflows/lib/__tests__/validate-mustache.test.ts
rename to keep-ui/entities/workflows/lib/__tests__/validate-mustache-ui-builder.test.ts
index 266afde6c2..7dd2327979 100644
--- a/keep-ui/entities/workflows/lib/__tests__/validate-mustache.test.ts
+++ b/keep-ui/entities/workflows/lib/__tests__/validate-mustache-ui-builder.test.ts
@@ -1,8 +1,8 @@
 import { Definition, V2ActionStep, V2Step } from "../../model/types";
 import {
-  validateAllMustacheVariablesInString,
-  validateMustacheVariableName,
-} from "../validate-definition";
+  validateAllMustacheVariablesForUIBuilderStep,
+  validateMustacheVariableForUIBuilderStep,
+} from "../validate-mustache-ui-builder";
 
 describe("validateMustacheVariableName", () => {
   const mockDefinition: Definition = {
@@ -40,8 +40,8 @@ describe("validateMustacheVariableName", () => {
   const mockSecrets = {};
 
   it("should validate alert variables", () => {
-    const result = validateMustacheVariableName(
-      "{{ alert.name }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "alert.name",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
@@ -50,8 +50,8 @@ describe("validateMustacheVariableName", () => {
   });
 
   it("should validate incident variables", () => {
-    const result = validateMustacheVariableName(
-      "{{ incident.title }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "incident.title",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
@@ -60,8 +60,8 @@ describe("validateMustacheVariableName", () => {
   });
 
   it("should validate step results access", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.First Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.First Step.results",
       mockDefinition.sequence[1],
       mockDefinition,
       mockSecrets
@@ -70,38 +70,38 @@ describe("validateMustacheVariableName", () => {
   });
 
   it("should prevent accessing current step results", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.First Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.First Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.First Step.results }}' - You can't access the results of the current step."
+      "Variable: 'steps.First Step.results' - You can't access the results of the current step."
     );
   });
 
   it("should prevent accessing future step results", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.Second Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.Second Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.Second Step.results }}' - You can't access the results of a step that appears after the current step."
+      "Variable: 'steps.Second Step.results' - You can't access the results of a step that appears after the current step."
     );
   });
 
   it("should prevent accessing action results from a step", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.Second Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.Second Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.Second Step.results }}' - You can't access the results of a step that appears after the current step."
+      "Variable: 'steps.Second Step.results' - You can't access the results of a step that appears after the current step."
     );
   });
 });
@@ -132,7 +132,7 @@ describe("validateAllMustacheVariablesInString", () => {
   const mockSecrets = {};
 
   it("should validate multiple variables in a string", () => {
-    const result = validateAllMustacheVariablesInString(
+    const result = validateAllMustacheVariablesForUIBuilderStep(
       "Alert: {{ alert.name }} with severity {{ alert.severity }}",
       mockDefinition.sequence[0],
       mockDefinition,
@@ -142,14 +142,14 @@ describe("validateAllMustacheVariablesInString", () => {
   });
 
   it("should detect invalid variables in a string", () => {
-    const result = validateAllMustacheVariablesInString(
+    const result = validateAllMustacheVariablesForUIBuilderStep(
       "Invalid: {{ invalid.var }} and {{ steps.Future Step.results }}",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toContain(
-      "Variable: '{{ steps.Future Step.results }}' - a 'Future Step' step that doesn't exist."
+      "Variable: 'steps.Future Step.results' - a 'Future Step' step that doesn't exist."
     );
   });
 
@@ -202,12 +202,96 @@ describe("validateAllMustacheVariablesInString", () => {
         consts: {},
       },
     };
-    const result = validateAllMustacheVariablesInString(
+    const result = validateAllMustacheVariablesForUIBuilderStep(
       "keep.dictget({{steps.python-step.results}} , '{{foreach.value.fingerprint}}', 'default') == '{{foreach.value.fingerprint}}'",
       telegramAction, // telegram step
       definition,
       mockSecrets
     );
     expect(result).toEqual([]);
+
+    // short syntax
+    const result2 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ . }}",
+      telegramAction, // telegram step
+      definition,
+      mockSecrets
+    );
+    expect(result2).toEqual([]);
+
+    const result3 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ foreach.value }}",
+      telegramAction,
+      definition,
+      mockSecrets
+    );
+    expect(result3).toEqual([]);
+
+    const result4 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ value }}",
+      telegramAction,
+      definition,
+      mockSecrets
+    );
+    expect(result4).toEqual([]);
+  });
+
+  it("should return an error if bracket notation is used", () => {
+    const result = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ steps['python-step'].results }}",
+      mockDefinition.sequence[0],
+      mockDefinition,
+      mockSecrets
+    );
+    expect(result).toEqual([
+      "Variable: 'steps[\'python-step\'].results' - bracket notation is not supported, use dot notation instead.",
+    ]);
+  });
+
+  it("should validate vars variables", () => {
+    const step: V2Step = {
+      id: "step1",
+      name: "First Step",
+      componentType: "task",
+      type: "step-test",
+      properties: {
+        actionParams: [],
+        stepParams: [],
+        vars: { test: "test" },
+      },
+    };
+    const definition = {
+      ...mockDefinition,
+      sequence: [step],
+    };
+    const result = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ vars.test }}",
+      step,
+      definition,
+      mockSecrets
+    );
+    expect(result).toEqual([]);
+  });
+
+  it("should return an error if vars variable is not found", () => {
+    const result = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ vars.test }}",
+      mockDefinition.sequence[0],
+      mockDefinition,
+      mockSecrets
+    );
+    expect(result).toEqual([
+      "Variable: 'vars.test' - Variable 'test' not found in step definition.",
+    ]);
+  });
+
+  it("should return an error for unknown variables", () => {
+    const result = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ unknown.var }}",
+      mockDefinition.sequence[0],
+      mockDefinition,
+      mockSecrets
+    );
+    expect(result).toEqual(["Variable: 'unknown.var' - unknown variable."]);
   });
 });
diff --git a/keep-ui/entities/workflows/lib/__tests__/validate-yaml.test.ts b/keep-ui/entities/workflows/lib/__tests__/validate-mustache-yaml.test.ts
similarity index 56%
rename from keep-ui/entities/workflows/lib/__tests__/validate-yaml.test.ts
rename to keep-ui/entities/workflows/lib/__tests__/validate-mustache-yaml.test.ts
index b95c4e7c2b..dbfb7fd1da 100644
--- a/keep-ui/entities/workflows/lib/__tests__/validate-yaml.test.ts
+++ b/keep-ui/entities/workflows/lib/__tests__/validate-mustache-yaml.test.ts
@@ -1,8 +1,30 @@
-import { validateMustacheVariableNameForYAML } from "../validate-yaml";
+import { validateMustacheVariableForYAMLStep } from "../validate-mustache-yaml";
 import { Provider } from "@/shared/api/providers";
 import { YamlWorkflowDefinition } from "../../model/yaml.types";
 
 describe("validateMustacheVariableNameForYAML", () => {
+  const stepWithVars = {
+    name: "step-with-vars",
+    provider: {
+      type: "step-test",
+      config: "test-config",
+      with: {},
+    },
+    vars: {
+      test: "test",
+    },
+  };
+  const stepWithForeach = {
+    name: "step-with-foreach",
+    foreach: "{{steps.First Step.results}}",
+    provider: {
+      type: "step-test",
+      config: "test-config",
+      with: {
+        param1: "{{.}}",
+      },
+    },
+  };
   const mockWorkflowDefinition: YamlWorkflowDefinition["workflow"] = {
     id: "test-workflow",
     name: "Test Workflow",
@@ -33,6 +55,8 @@ describe("validateMustacheVariableNameForYAML", () => {
           },
         },
       },
+      stepWithForeach,
+      stepWithVars,
     ],
   };
 
@@ -72,6 +96,30 @@ describe("validateMustacheVariableNameForYAML", () => {
       linked: true,
       last_alert_received: "",
     },
+    {
+      type: "notrequiringinstallation",
+      config: {},
+      details: {
+        name: "test-config",
+        authentication: {
+          api_key: "test-key",
+        },
+      },
+      id: "test-provider",
+      display_name: "Test Provider",
+      can_query: false,
+      can_notify: false,
+      tags: [],
+      validatedScopes: {},
+      pulling_available: false,
+      pulling_enabled: true,
+      categories: ["Others"],
+      coming_soon: false,
+      health: false,
+      installed: false,
+      linked: false,
+      last_alert_received: "",
+    },
   ];
 
   const mockInstalledProviders: Provider[] = [
@@ -109,7 +157,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   ];
 
   it("should detect empty variable name", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -122,7 +170,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should detect empty path parts", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "step..results",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -132,13 +180,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: step..results - path parts cannot be empty.",
+      "Variable: 'step..results' - path parts cannot be empty.",
       "warning",
     ]);
   });
 
   it("should validate alert variables", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "alert.name",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -151,7 +199,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should validate incident variables", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "incident.title",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -164,7 +212,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should validate valid secrets", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "secrets.API_KEY",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -177,7 +225,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should detect missing secret name", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "secrets.",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -187,13 +235,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: secrets. - path parts cannot be empty.",
+      "Variable: 'secrets.' - path parts cannot be empty.",
       "warning",
     ]);
   });
 
   it("should detect non-existent secret", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "secrets.MISSING_KEY",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -203,13 +251,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      'Variable: secrets.MISSING_KEY - Secret "MISSING_KEY" not found.',
+      "Variable: 'secrets.MISSING_KEY' - Secret 'MISSING_KEY' not found.",
       "error",
     ]);
   });
 
   it("should validate provider access", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "providers.test-config",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -222,8 +270,8 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should validate default provider access", () => {
-    const result = validateMustacheVariableNameForYAML(
-      "providers.default-test",
+    const result = validateMustacheVariableForYAMLStep(
+      "providers.default-notrequiringinstallation",
       mockWorkflowDefinition!.steps![0],
       "step",
       mockWorkflowDefinition,
@@ -235,7 +283,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should detect missing provider name", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "providers.",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -245,13 +293,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: providers. - path parts cannot be empty.",
+      "Variable: 'providers.' - path parts cannot be empty.",
       "warning",
     ]);
   });
 
   it("should detect non-existent default provider", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "providers.default-nonexistent",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -261,13 +309,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      'Variable: providers.default-nonexistent - Provider "default-nonexistent" not found.',
+      "Variable: 'providers.default-nonexistent' - Unknown provider type 'nonexistent'.",
       "warning",
     ]);
   });
 
   it("should detect non-installed provider", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "providers.nonexistent-config",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -277,13 +325,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      'Variable: providers.nonexistent-config - Provider "nonexistent-config" is not installed.',
+      "Variable: 'providers.nonexistent-config' - Provider 'nonexistent-config' is not installed.",
       "warning",
     ]);
   });
 
   it("should validate step results access", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.First Step.results",
       mockWorkflowDefinition!.steps![1],
       "step",
@@ -296,7 +344,7 @@ describe("validateMustacheVariableNameForYAML", () => {
   });
 
   it("should detect missing step name", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -306,13 +354,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: steps. - path parts cannot be empty.",
+      "Variable: 'steps.' - path parts cannot be empty.",
       "warning",
     ]);
   });
 
   it("should detect non-existent step", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.Nonexistent Step.results",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -322,13 +370,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      'Variable: steps.Nonexistent Step.results - a "Nonexistent Step" step doesn\'t exist.',
+      "Variable: 'steps.Nonexistent Step.results' - a 'Nonexistent Step' step doesn't exist.",
       "error",
     ]);
   });
 
   it("should prevent accessing current step results", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.First Step.results",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -338,13 +386,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: steps.First Step.results - You can't access the results of the current step.",
+      "Variable: 'steps.First Step.results' - You can't access the results of the current step.",
       "error",
     ]);
   });
 
   it("should prevent accessing future step results", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.Second Step.results",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -354,13 +402,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      "Variable: steps.Second Step.results - You can't access the results of a step that appears after the current step.",
+      "Variable: 'steps.Second Step.results' - You can't access the results of a step that appears after the current step.",
       "error",
     ]);
   });
 
   it("should detect missing results suffix", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "steps.First Step.output",
       mockWorkflowDefinition!.steps![1],
       "step",
@@ -370,13 +418,13 @@ describe("validateMustacheVariableNameForYAML", () => {
       mockInstalledProviders
     );
     expect(result).toEqual([
-      'Variable: steps.First Step.output - To access the results of a step, use "results" as suffix.',
+      "Variable: 'steps.First Step.output' - To access the results of a step, use 'results' as suffix.",
       "warning",
     ]);
   });
 
   it("should skip provider validation when providers are not available", () => {
-    const result = validateMustacheVariableNameForYAML(
+    const result = validateMustacheVariableForYAMLStep(
       "providers.test-config",
       mockWorkflowDefinition!.steps![0],
       "step",
@@ -387,4 +435,108 @@ describe("validateMustacheVariableNameForYAML", () => {
     );
     expect(result).toBeNull();
   });
+
+  it("should return an error if bracket notation is used", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      "steps['python-step'].results",
+      mockWorkflowDefinition!.steps![0],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toEqual([
+      "Variable: 'steps[\'python-step\'].results' - bracket notation is not supported, use dot notation instead.",
+      "warning",
+    ]);
+  });
+
+  it("should allow {{.}} syntax in steps with foreach", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      ".",
+      mockWorkflowDefinition!.steps![2],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toBeNull();
+  });
+
+  it("should return an error if {{.}} syntax is used in step without foreach", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      ".",
+      mockWorkflowDefinition!.steps![0],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toEqual([
+      "Variable: '.' - short syntax can only be used in a step with foreach.",
+      "warning",
+    ]);
+  });
+
+  it("should return an error if foreach or value is used in a step without foreach", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      "foreach.value",
+      mockWorkflowDefinition!.steps![0],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toEqual([
+      "Variable: 'foreach.value' - 'foreach' can only be used in a step with foreach.",
+      "warning",
+    ]);
+
+    const result2 = validateMustacheVariableForYAMLStep(
+      "value",
+      mockWorkflowDefinition!.steps![0],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result2).toEqual([
+      "Variable: 'value' - 'value' can only be used in a step with foreach.",
+      "warning",
+    ]);
+  });
+
+  it("should validate vars variable", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      "vars.test",
+      stepWithVars,
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toBeNull();
+  });
+
+  it("should return an error if vars variable is not found", () => {
+    const result = validateMustacheVariableForYAMLStep(
+      "vars.test",
+      mockWorkflowDefinition!.steps![0],
+      "step",
+      mockWorkflowDefinition,
+      mockSecrets,
+      mockProviders,
+      mockInstalledProviders
+    );
+    expect(result).toEqual([
+      "Variable: 'vars.test' - Variable 'test' not found in step definition.",
+      "error",
+    ]);
+  });
 });
diff --git a/keep-ui/entities/workflows/lib/__tests__/validation.test.ts b/keep-ui/entities/workflows/lib/__tests__/validation.test.ts
index 3ca5705ead..d143e54ca0 100644
--- a/keep-ui/entities/workflows/lib/__tests__/validation.test.ts
+++ b/keep-ui/entities/workflows/lib/__tests__/validation.test.ts
@@ -165,7 +165,10 @@ describe("validateStepPure", () => {
       mockProviders,
       mockInstalledProviders,
       mockSecrets,
-      mockDefinition
+      {
+        ...mockDefinition,
+        sequence: [step],
+      }
     );
     expect(result).toEqual([]);
   });
diff --git a/keep-ui/entities/workflows/lib/mustache.ts b/keep-ui/entities/workflows/lib/mustache.ts
index 544afc7629..450edddbd0 100644
--- a/keep-ui/entities/workflows/lib/mustache.ts
+++ b/keep-ui/entities/workflows/lib/mustache.ts
@@ -1,4 +1,5 @@
-const MUSTACHE_REGEX = /\{\{\s*(.*?)\s*\}\}/g;
+export const MUSTACHE_REGEX = /\{\{\s*(.*?)\s*\}\}/g;
+export const ALLOWED_MUSTACHE_VARIABLE_REGEX = /^[a-zA-Z0-9._-\s]+$/;
 
 /**
  * Extracts all mustache variables from a string.
@@ -16,3 +17,17 @@ export function extractMustacheVariables(yamlString: string): string[] {
       .filter((variable) => variable.length > 0 && !variable.endsWith("."))
   );
 }
+
+/**
+ * Extracts the trimmed value from mustache syntax by removing curly brackets.
+ *
+ * @param mustacheString - A string containing mustache syntax like "{{ variable }}"
+ * @returns The trimmed inner value without curly brackets
+ */
+export function extractMustacheValue(mustacheString: string): string {
+  // Use regex to match content between {{ and }} and trim whitespace
+  const match = mustacheString.match(/\{\{\s*(.*?)\s*\}\}/);
+
+  // Return the captured group if found, otherwise return empty string
+  return match ? match[1] : "";
+}
diff --git a/keep-ui/entities/workflows/lib/validate-definition.ts b/keep-ui/entities/workflows/lib/validate-definition.ts
index ea5af48dd1..7f3e2912a9 100644
--- a/keep-ui/entities/workflows/lib/validate-definition.ts
+++ b/keep-ui/entities/workflows/lib/validate-definition.ts
@@ -1,150 +1,10 @@
 import { Provider } from "@/shared/api/providers";
 import { Definition, V2Step } from "../model/types";
 import { getWithParams } from "./parser";
+import { validateAllMustacheVariablesForUIBuilderStep } from "./validate-mustache-ui-builder";
 
 export type ValidationResult = [string, string];
 export type ValidationError = [string, "error" | "warning" | "info"];
-/**
- * Extracts the trimmed value from mustache syntax by removing curly brackets.
- *
- * @param mustacheString - A string containing mustache syntax like "{{ variable }}"
- * @returns The trimmed inner value without curly brackets
- */
-function extractMustacheValue(mustacheString: string): string {
-  // Use regex to match content between {{ and }} and trim whitespace
-  const match = mustacheString.match(/\{\{\s*(.*?)\s*\}\}/);
-
-  // Return the captured group if found, otherwise return empty string
-  return match ? match[1] : "";
-}
-
-function flattenSequence(sequence: V2Step[]): V2Step[] {
-  const flatSequence: V2Step[] = [];
-  for (const step of sequence) {
-    if (step.componentType === "container") {
-      flatSequence.push(step);
-      flatSequence.push(...flattenSequence(step.sequence || []));
-    } else {
-      flatSequence.push(step);
-    }
-  }
-  return flatSequence;
-}
-
-export const validateMustacheVariableName = (
-  variableName: string,
-  currentStep: V2Step,
-  definition: Definition,
-  secrets: Record<string, string>
-) => {
-  const flatSequence = flattenSequence(definition.sequence);
-  const cleanedVariableName = extractMustacheValue(variableName);
-  const parts = cleanedVariableName.split(".");
-  if (!parts.every((part) => part.length > 0)) {
-    return `Variable: '${variableName}' - Parts cannot be empty.`;
-  }
-  if (parts[0] === "alert") {
-    // todo: validate alert properties
-    return null;
-  }
-  if (parts[0] === "incident") {
-    // todo: validate incident properties
-    return null;
-  }
-  if (parts[0] === "secrets") {
-    const secretName = parts[1];
-    if (!secretName) {
-      return `Variable: '${variableName}' - To access a secret, you need to specify the secret name.`;
-    }
-    if (!secrets[secretName]) {
-      return `Variable: '${variableName}' - Secret '${secretName}' not found.`;
-    }
-    return null;
-  }
-  if (parts[0] === "consts") {
-    const constName = parts[1];
-    if (!constName) {
-      return `Variable: '${variableName}' - To access a constant, you need to specify the constant name.`;
-    }
-    if (!definition.properties.consts?.[constName]) {
-      return `Variable: '${variableName}' - Constant '${constName}' not found.`;
-    }
-  }
-  if (parts[0] === "steps") {
-    const stepName = parts[1];
-    if (!stepName) {
-      return `Variable: '${variableName}' - To access the results of a step, you need to specify the step name.`;
-    }
-    // todo: check if
-    // - the step exists
-    // - it's not the current step (can't access own results, only enrich_alert and enrich_incident can access their own results)
-    // - it's above the current step
-    // - if it's a step it cannot access actions since they run after steps
-    const step = flatSequence.find(
-      (step) => step.id === stepName || step.name === stepName
-    );
-    const stepIndex = flatSequence.findIndex(
-      (step) => step.id === stepName || step.name === stepName
-    );
-    const currentStepIndex = flatSequence.findIndex(
-      (step) => step.id === currentStep.id
-    );
-    if (!step) {
-      return `Variable: '${variableName}' - a '${stepName}' step that doesn't exist.`;
-    }
-    const isCurrentStep = step.id === currentStep.id;
-    if (isCurrentStep) {
-      return `Variable: '${variableName}' - You can't access the results of the current step.`;
-    }
-    if (stepIndex > currentStepIndex) {
-      return `Variable: '${variableName}' - You can't access the results of a step that appears after the current step.`;
-    }
-    if (
-      currentStep.type.startsWith("step-") &&
-      step.type.startsWith("action-")
-    ) {
-      return `Variable: '${variableName}' - You can't access the results of an action from a step.`;
-    }
-
-    if (
-      parts[2] === "results" ||
-      parts[2].startsWith("results.") ||
-      parts[2].startsWith("results[")
-    ) {
-      // todo: validate results properties
-      return null;
-    } else {
-      return `Variable: '${variableName}' - To access the results of a step, use 'results' as suffix.`;
-    }
-  }
-  return null;
-};
-
-export const validateAllMustacheVariablesInString = (
-  string: string,
-  currentStep: V2Step,
-  definition: Definition,
-  secrets: Record<string, string>
-) => {
-  const regex = /\{\{([^}]+)\}\}/g;
-  const matches = string.match(regex);
-  if (!matches) {
-    return [];
-  }
-  const errors: string[] = [];
-  matches.forEach((match) => {
-    const error = validateMustacheVariableName(
-      match,
-      currentStep,
-      definition,
-      secrets
-    );
-    if (error) {
-      errors.push(error);
-    }
-  });
-  return errors;
-};
 
 export const checkProviderNeedsInstallation = (
   providerObject: Pick<Provider, "type" | "config">
@@ -278,7 +138,7 @@ export function validateStepPure(
     (step.componentType === "task" || step.componentType === "container") &&
     step.properties.if
   ) {
-    const variableErrors = validateAllMustacheVariablesInString(
+    const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
       step.properties.if,
       step,
       definition,
@@ -290,7 +150,7 @@ export function validateStepPure(
   }
   if (step.componentType === "task" && step.properties.with?.enrich_alert) {
     const values = step.properties.with.enrich_alert.map((item) => item.value);
-    const variableErrors = validateAllMustacheVariablesInString(
+    const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
       values.join(","),
       step,
       definition,
@@ -304,7 +164,7 @@ export function validateStepPure(
     const values = step.properties.with.enrich_incident.map(
       (item) => item.value
     );
-    const variableErrors = validateAllMustacheVariablesInString(
+    const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
       values.join(","),
       step,
       definition,
@@ -322,7 +182,7 @@ export function validateStepPure(
       if (!step.properties.value) {
         validationErrors.push(["Condition value cannot be empty.", "error"]);
       }
-      const variableErrorsValue = validateAllMustacheVariablesInString(
+      const variableErrorsValue = validateAllMustacheVariablesForUIBuilderStep(
         step.properties.value?.toString() ?? "",
         step,
         definition,
@@ -337,12 +197,13 @@ export function validateStepPure(
           "error",
         ]);
       }
-      const variableErrorsCompareTo = validateAllMustacheVariablesInString(
-        step.properties.compare_to?.toString() ?? "",
-        step,
-        definition,
-        secrets
-      );
+      const variableErrorsCompareTo =
+        validateAllMustacheVariablesForUIBuilderStep(
+          step.properties.compare_to?.toString() ?? "",
+          step,
+          definition,
+          secrets
+        );
       variableErrorsCompareTo.forEach((error) => {
         validationErrors.push([error, "error"]);
       });
@@ -351,7 +212,7 @@ export function validateStepPure(
       if (!step.properties.assert) {
         validationErrors.push(["Condition assert cannot be empty.", "error"]);
       }
-      const variableErrors = validateAllMustacheVariablesInString(
+      const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
         step.properties.assert,
         step,
         definition,
@@ -397,7 +258,7 @@ export function validateStepPure(
     }
     for (const [key, value] of Object.entries(withParams)) {
       if (typeof value === "string") {
-        const variableErrors = validateAllMustacheVariablesInString(
+        const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
           value,
           step,
           definition,
@@ -413,7 +274,7 @@ export function validateStepPure(
     if (!step.properties.value) {
       validationErrors.push(["Foreach value cannot be empty.", "error"]);
     }
-    const variableErrors = validateAllMustacheVariablesInString(
+    const variableErrors = validateAllMustacheVariablesForUIBuilderStep(
       step.properties.value,
       step,
       definition,
diff --git a/keep-ui/entities/workflows/lib/validate-mustache-ui-builder.ts b/keep-ui/entities/workflows/lib/validate-mustache-ui-builder.ts
new file mode 100644
index 0000000000..b51eaba3eb
--- /dev/null
+++ b/keep-ui/entities/workflows/lib/validate-mustache-ui-builder.ts
@@ -0,0 +1,211 @@
+/**
+ * @fileoverview
+ * Validates a mustache variable name in a UI builder, it's slightly different from the YAML validator because UI builder has a different structure, like nested steps, etc.
+ * TODO: refactor to share code with the YAML validator
+ */
+
+import { V2Step, Definition } from "../model/types";
+import { ALLOWED_MUSTACHE_VARIABLE_REGEX, MUSTACHE_REGEX } from "./mustache";
+
+type V2StepWithParentId = V2Step & { parentId?: string };
+
+/*
+ * Validates a mustache variable name in a UI builder.
+ *
+ * @param cleanedVariableName - Mustache variable name without curly brackets.
+ * @param currentStep - The current step in the sequence in workflow-store format (V2Step + {parentId: string} for loops)
+ * @param definition - The definition of the workflow in workflow-store format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @returns An error message if the variable name is invalid, otherwise null.
+ */
+export const validateMustacheVariableForUIBuilderStep = (
+  cleanedVariableName: string,
+  _currentStep: V2Step,
+  definition: Definition,
+  secrets: Record<string, string>
+): string | null => {
+  const flatSequence = flattenSequence(definition.sequence);
+  const currentStep = flatSequence.find(
+    (step) => step.name === _currentStep.name || step.id === _currentStep.id
+  );
+  if (!currentStep) {
+    // wtf exception, should never happen
+    throw new Error("Current step not found in the sequence");
+  }
+  console.log("flatSequence", flatSequence);
+  if (!cleanedVariableName) {
+    return "Empty mustache variable.";
+  }
+  if (cleanedVariableName === ".") {
+    if (currentStep.parentId) {
+      return null;
+    }
+    return `Variable: '${cleanedVariableName}' - needs to be used in a for loop.`;
+  }
+  if (!ALLOWED_MUSTACHE_VARIABLE_REGEX.test(cleanedVariableName)) {
+    if (
+      cleanedVariableName.includes("[") ||
+      cleanedVariableName.includes("]")
+    ) {
+      return `Variable: '${cleanedVariableName}' - bracket notation is not supported, use dot notation instead.`;
+    }
+    return `Variable: '${cleanedVariableName}' - contains invalid characters.`;
+  }
+  const parts = cleanedVariableName.split(".");
+  if (!parts.every((part) => part.length > 0)) {
+    return `Variable: '${cleanedVariableName}' - Parts cannot be empty.`;
+  }
+  if (parts[0] === "foreach") {
+    if (currentStep.parentId) {
+      return null;
+    }
+    return `Variable: '${cleanedVariableName}' - 'foreach' can only be used in a step with foreach.`;
+  }
+  if (parts[0] === "value") {
+    if (currentStep.parentId) {
+      return null;
+    }
+    return `Variable: '${cleanedVariableName}' - 'value' can only be used in a step with foreach.`;
+  }
+  if (parts[0] === "alert") {
+    // todo: validate alert properties
+    return null;
+  }
+  if (parts[0] === "incident") {
+    // todo: validate incident properties
+    return null;
+  }
+  if (parts[0] === "secrets") {
+    const secretName = parts[1];
+    if (!secretName) {
+      return `Variable: '${cleanedVariableName}' - To access a secret, you need to specify the secret name.`;
+    }
+    if (!secrets[secretName]) {
+      return `Variable: '${cleanedVariableName}' - Secret '${secretName}' not found.`;
+    }
+    return null;
+  }
+  if (parts[0] === "vars") {
+    const varName = parts?.[1];
+    if (!varName) {
+      return `Variable: '${cleanedVariableName}' - To access a variable, you need to specify the variable name.`;
+    }
+    if (
+      currentStep.componentType !== "task" ||
+      !currentStep.properties.vars?.[varName]
+    ) {
+      return `Variable: '${cleanedVariableName}' - Variable '${varName}' not found in step definition.`;
+    }
+    return null;
+  }
+  if (parts[0] === "consts") {
+    const constName = parts[1];
+    if (!constName) {
+      return `Variable: '${cleanedVariableName}' - To access a constant, you need to specify the constant name.`;
+    }
+    if (!definition.properties.consts?.[constName]) {
+      return `Variable: '${cleanedVariableName}' - Constant '${constName}' not found.`;
+    }
+  }
+  if (parts[0] === "steps") {
+    const stepName = parts[1];
+    if (!stepName) {
+      return `Variable: '${cleanedVariableName}' - To access the results of a step, you need to specify the step name.`;
+    }
+    // todo: check if
+    // - the step exists
+    // - it's not the current step (can't access own results, only enrich_alert and enrich_incident can access their own results)
+    // - it's above the current step
+    // - if it's a step it cannot access actions since they run after steps
+    const step = flatSequence.find(
+      (step) => step.id === stepName || step.name === stepName
+    );
+    const stepIndex = flatSequence.findIndex(
+      (step) => step.id === stepName || step.name === stepName
+    );
+    const currentStepIndex = flatSequence.findIndex(
+      (step) => step.id === currentStep.id
+    );
+    if (!step) {
+      return `Variable: '${cleanedVariableName}' - a '${stepName}' step that doesn't exist.`;
+    }
+    const isCurrentStep = step.id === currentStep.id;
+    if (isCurrentStep) {
+      return `Variable: '${cleanedVariableName}' - You can't access the results of the current step.`;
+    }
+    if (stepIndex > currentStepIndex) {
+      return `Variable: '${cleanedVariableName}' - You can't access the results of a step that appears after the current step.`;
+    }
+    if (
+      currentStep.type.startsWith("step-") &&
+      step.type.startsWith("action-")
+    ) {
+      return `Variable: '${cleanedVariableName}' - You can't access the results of an action from a step.`;
+    }
+
+    if (
+      parts.length > 2 &&
+      (parts[2] === "results" ||
+        parts[2].startsWith("results.") ||
+        parts[2].startsWith("results["))
+    ) {
+      // todo: validate results properties
+      return null;
+    } else {
+      return `Variable: '${cleanedVariableName}' - To access the results of a step, use 'results' as suffix.`;
+    }
+  }
+  return `Variable: '${cleanedVariableName}' - unknown variable.`;
+};
+
+function flattenSequence(
+  sequence: V2Step[],
+  parentId?: string
+): V2StepWithParentId[] {
+  const flatSequence: V2StepWithParentId[] = [];
+  for (const step of sequence) {
+    const stepWithParentId = { ...step, parentId };
+    if (step.componentType === "container") {
+      flatSequence.push(stepWithParentId);
+      flatSequence.push(...flattenSequence(step.sequence || [], step.id));
+    } else {
+      flatSequence.push(stepWithParentId);
+    }
+  }
+  return flatSequence;
+}
+
+/**
+ * Validates all mustache variables in a string.
+ *
+ * @param string - The string to validate.
+ * @param currentStep - The current step in the sequence in workflow-store format.
+ * @param definition - The definition of the workflow in workflow-store format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @returns An array of error messages if the variable names are invalid, otherwise an empty array.
+ */
+export const validateAllMustacheVariablesForUIBuilderStep = (
+  string: string,
+  currentStep: V2Step,
+  definition: Definition,
+  secrets: Record<string, string>
+) => {
+  const matches = [...string.matchAll(MUSTACHE_REGEX)];
+  if (!matches) {
+    return [];
+  }
+  const errors: string[] = [];
+  matches.forEach((matchExecArray) => {
+    const match = matchExecArray[1];
+    const error = validateMustacheVariableForUIBuilderStep(
+      match,
+      currentStep,
+      definition,
+      secrets
+    );
+    if (error) {
+      errors.push(error);
+    }
+  });
+  return errors;
+};
diff --git a/keep-ui/entities/workflows/lib/validate-mustache-yaml.ts b/keep-ui/entities/workflows/lib/validate-mustache-yaml.ts
new file mode 100644
index 0000000000..a04c9be354
--- /dev/null
+++ b/keep-ui/entities/workflows/lib/validate-mustache-yaml.ts
@@ -0,0 +1,265 @@
+/**
+ * @fileoverview
+ * Validates a mustache variable name in a YAML workflow definition.
+ * TODO: refactor to share code with the UI builder validator
+ */
+
+import { YamlStepOrAction, YamlWorkflowDefinition } from "../model/yaml.types";
+import { Provider } from "@/shared/api/providers";
+import { ALLOWED_MUSTACHE_VARIABLE_REGEX } from "./mustache";
+import { checkProviderNeedsInstallation } from "./validate-definition";
+
+/**
+ * Validates a mustache variable name in a YAML workflow definition.
+ *
+ * @param cleanedVariableName - Mustache variable name without curly brackets.
+ * @param currentStep - The current step in the sequence in YAML format.
+ * @param currentStepType - The type of the current step.
+ * @param definition - The definition of the workflow in YAML format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @param providers - The providers of the workflow. This is used to validate providers.
+ * @param installedProviders - The installed providers of the workflow. This is used to validate installed providers.
+ * @returns An [error message, "error" | "warning" | "info"] if the variable name is invalid, otherwise null.
+ */
+export const validateMustacheVariableForYAMLStep = (
+  cleanedVariableName: string,
+  currentStep: YamlStepOrAction,
+  currentStepType: "step" | "action",
+  definition: YamlWorkflowDefinition["workflow"],
+  secrets: Record<string, string>,
+  providers: Provider[] | null,
+  installedProviders: Provider[] | null
+): [string, "error" | "warning" | "info"] | null => {
+  if (!cleanedVariableName) {
+    return ["Empty mustache variable.", "warning"];
+  }
+  if (cleanedVariableName === ".") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - short syntax can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (!ALLOWED_MUSTACHE_VARIABLE_REGEX.test(cleanedVariableName)) {
+    if (
+      cleanedVariableName.includes("[") ||
+      cleanedVariableName.includes("]")
+    ) {
+      return [
+        `Variable: '${cleanedVariableName}' - bracket notation is not supported, use dot notation instead.`,
+        "warning",
+      ];
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - contains invalid characters.`,
+      "warning",
+    ];
+  }
+  const parts = cleanedVariableName.split(".");
+  if (!parts.every((part) => part.length > 0)) {
+    return [
+      `Variable: '${cleanedVariableName}' - path parts cannot be empty.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "foreach") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'foreach' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "value") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'value' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "providers") {
+    const providerName = parts[1];
+    if (!providerName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a provider, you need to specify the provider name.`,
+        "warning",
+      ];
+    }
+    if (!providers || !installedProviders) {
+      // Skip validation if providers or installedProviders are not available
+      return null;
+    }
+    const isDefault = providerName.startsWith("default-");
+    if (isDefault) {
+      const providerType = isDefault ? providerName.split("-")[1] : null;
+      const provider = providers.find((p) => p.type === providerType);
+      if (!provider) {
+        return [
+          `Variable: '${cleanedVariableName}' - Unknown provider type '${providerType}'.`,
+          "warning",
+        ];
+      }
+      const doesProviderNeedInstallation =
+        checkProviderNeedsInstallation(provider);
+      const installedProvider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (doesProviderNeedInstallation && !installedProvider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    } else {
+      const provider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (!provider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    }
+    return null;
+  }
+  if (parts[0] === "alert") {
+    // todo: validate alert properties
+    return null;
+  }
+  if (parts[0] === "incident") {
+    // todo: validate incident properties
+    return null;
+  }
+  if (parts[0] === "secrets") {
+    const secretName = parts[1];
+    if (!secretName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a secret, you need to specify the secret name.`,
+        "warning",
+      ];
+    }
+    if (!secrets[secretName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Secret '${secretName}' not found.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "vars") {
+    const varName = parts?.[1];
+    if (!varName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a variable, you need to specify the variable name.`,
+        "warning",
+      ];
+    }
+    if (!currentStep.vars?.[varName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Variable '${varName}' not found in step definition.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "consts") {
+    const constName = parts[1];
+    if (!constName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a constant, you need to specify the constant name.`,
+        "warning",
+      ];
+    }
+    if (!definition.consts?.[constName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Constant '${constName}' not found.`,
+        "error",
+      ];
+    }
+  }
+  if (parts[0] === "steps") {
+    const stepName = parts[1];
+    if (!stepName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access the results of a step, you need to specify the step name.`,
+        "warning",
+      ];
+    }
+    // todo: check if
+    // - the step exists
+    // - it's not the current step (can't access own results, only enrich_alert and enrich_incident can access their own results)
+    // - it's above the current step
+    // - if it's a step it cannot access actions since they run after steps
+    const step = definition.steps?.find((s) => s.name === stepName);
+    const stepIndex =
+      definition.steps?.findIndex((s) => s.name === stepName) ?? -1;
+    const currentStepIndex =
+      currentStepType === "step"
+        ? (definition.steps?.findIndex((s) => s.name === currentStep.name) ??
+          -1)
+        : -1;
+    if (!step || stepIndex === -1) {
+      return [
+        `Variable: '${cleanedVariableName}' - a '${stepName}' step doesn't exist.`,
+        "error",
+      ];
+    }
+    const isCurrentStep = step.name === currentStep.name;
+    if (isCurrentStep) {
+      return [
+        `Variable: '${cleanedVariableName}' - You can't access the results of the current step.`,
+        "error",
+      ];
+    }
+    if (currentStepIndex !== -1 && stepIndex > currentStepIndex) {
+      return [
+        `Variable: '${cleanedVariableName}' - You can't access the results of a step that appears after the current step.`,
+        "error",
+      ];
+    }
+
+    if (!definition.steps?.some((step) => step.name === stepName)) {
+      return [
+        `Variable: '${cleanedVariableName}' - a '${stepName}' step that doesn't exist.`,
+        "error",
+      ];
+    }
+    if (
+      parts.length > 2 &&
+      (parts[2] === "results" ||
+        parts[2].startsWith("results.") ||
+        parts[2].startsWith("results["))
+    ) {
+      // todo: validate results properties
+      return null;
+    } else {
+      return [
+        `Variable: '${cleanedVariableName}' - To access the results of a step, use 'results' as suffix.`,
+        "warning",
+      ];
+    }
+  }
+  return [`Variable: '${cleanedVariableName}' - unknown variable.`, "warning"];
+};
diff --git a/keep-ui/entities/workflows/lib/validate-yaml.ts b/keep-ui/entities/workflows/lib/validate-yaml.ts
deleted file mode 100644
index c1ee1b2052..0000000000
--- a/keep-ui/entities/workflows/lib/validate-yaml.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { YamlStepOrAction, YamlWorkflowDefinition } from "../model/yaml.types";
-import { Provider } from "@/shared/api/providers";
-
-export const validateMustacheVariableNameForYAML = (
-  cleanedVariableName: string,
-  currentStep: YamlStepOrAction,
-  currentStepType: "step" | "action",
-  definition: YamlWorkflowDefinition["workflow"],
-  secrets: Record<string, string>,
-  providers: Provider[] | null,
-  installedProviders: Provider[] | null
-) => {
-  if (!cleanedVariableName) {
-    return ["Empty mustache variable.", "warning"];
-  }
-  const parts = cleanedVariableName.split(".");
-  if (!parts.every((part) => part.length > 0)) {
-    return [
-      `Variable: ${cleanedVariableName} - path parts cannot be empty.`,
-      "warning",
-    ];
-  }
-  if (parts[0] === "providers") {
-    const providerName = parts[1];
-    if (!providerName) {
-      return [
-        `Variable: ${cleanedVariableName} - To access a provider, you need to specify the provider name.`,
-        "warning",
-      ];
-    }
-    if (!providers || !installedProviders) {
-      // Skip validation if providers or installedProviders are not available
-      return null;
-    }
-    const isDefault = providerName.startsWith("default-");
-    if (isDefault) {
-      const providerType = isDefault ? providerName.split("-")[1] : null;
-      const provider = providers.find((p) => p.type === providerType);
-      if (!provider) {
-        return [
-          `Variable: ${cleanedVariableName} - Provider "${providerName}" not found.`,
-          "warning",
-        ];
-      }
-    } else {
-      const provider = installedProviders.find(
-        (p) => p.details.name === providerName
-      );
-      if (!provider) {
-        return [
-          `Variable: ${cleanedVariableName} - Provider "${providerName}" is not installed.`,
-          "warning",
-        ];
-      }
-    }
-    return null;
-  }
-  if (parts[0] === "alert") {
-    // todo: validate alert properties
-    return null;
-  }
-  if (parts[0] === "incident") {
-    // todo: validate incident properties
-    return null;
-  }
-  if (parts[0] === "secrets") {
-    const secretName = parts[1];
-    if (!secretName) {
-      return [
-        `Variable: ${cleanedVariableName} - To access a secret, you need to specify the secret name.`,
-        "warning",
-      ];
-    }
-    if (!secrets[secretName]) {
-      return [
-        `Variable: ${cleanedVariableName} - Secret "${secretName}" not found.`,
-        "error",
-      ];
-    }
-    return null;
-  }
-  if (parts[0] === "consts") {
-    const constName = parts[1];
-    if (!constName) {
-      return [
-        `Variable: ${cleanedVariableName} - To access a constant, you need to specify the constant name.`,
-      ];
-    }
-    if (!definition.consts?.[constName]) {
-      return [
-        `Variable: ${cleanedVariableName} - Constant "${constName}" not found.`,
-        "error",
-      ];
-    }
-  }
-  if (parts[0] === "steps") {
-    const stepName = parts[1];
-    if (!stepName) {
-      return [
-        `Variable: ${cleanedVariableName} - To access the results of a step, you need to specify the step name.`,
-        "warning",
-      ];
-    }
-    // todo: check if
-    // - the step exists
-    // - it's not the current step (can't access own results, only enrich_alert and enrich_incident can access their own results)
-    // - it's above the current step
-    // - if it's a step it cannot access actions since they run after steps
-    const step = definition.steps?.find((s) => s.name === stepName);
-    const stepIndex =
-      definition.steps?.findIndex((s) => s.name === stepName) ?? -1;
-    const currentStepIndex =
-      currentStepType === "step"
-        ? (definition.steps?.findIndex((s) => s.name === currentStep.name) ??
-          -1)
-        : -1;
-    if (!step || stepIndex === -1) {
-      return [
-        `Variable: ${cleanedVariableName} - a "${stepName}" step doesn't exist.`,
-        "error",
-      ];
-    }
-    const isCurrentStep = step.name === currentStep.name;
-    if (isCurrentStep) {
-      return [
-        `Variable: ${cleanedVariableName} - You can't access the results of the current step.`,
-        "error",
-      ];
-    }
-    if (currentStepIndex !== -1 && stepIndex > currentStepIndex) {
-      return [
-        `Variable: ${cleanedVariableName} - You can't access the results of a step that appears after the current step.`,
-        "error",
-      ];
-    }
-
-    if (!definition.steps?.some((step) => step.name === stepName)) {
-      return [
-        `Variable: ${cleanedVariableName} - a "${stepName}" step that doesn't exist.`,
-        "error",
-      ];
-    }
-    if (
-      parts[2] === "results" ||
-      parts[2].startsWith("results.") ||
-      parts[2].startsWith("results[")
-    ) {
-      // todo: validate results properties
-      return null;
-    } else {
-      return [
-        `Variable: ${cleanedVariableName} - To access the results of a step, use "results" as suffix.`,
-        "warning",
-      ];
-    }
-  }
-  return null;
-};
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/useYamlValidation.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/useYamlValidation.ts
index 21e0d8d1f3..42b3483ba7 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/useYamlValidation.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/useYamlValidation.ts
@@ -2,8 +2,11 @@ import { useCallback, useState } from "react";
 import { parseDocument } from "yaml";
 import type { editor, Uri } from "monaco-editor";
 import { MarkerSeverity, getSeverityString } from "./utils";
-import { YamlValidationError } from "../model/types";
-import { validateMustacheVariableNameForYAML } from "@/entities/workflows/lib/validate-yaml";
+import {
+  YamlValidationError,
+  YamlValidationErrorSeverity,
+} from "../model/types";
+import { validateMustacheVariableForYAMLStep } from "@/entities/workflows/lib/validate-mustache-yaml";
 import {
   getCurrentPath,
   parseWorkflowYamlStringToJSON,
@@ -16,6 +19,12 @@ interface UseYamlValidationProps {
   >;
 }
 
+const SEVERITY_MAP = {
+  error: MarkerSeverity.Error,
+  warning: MarkerSeverity.Warning,
+  info: MarkerSeverity.Hint,
+};
+
 export interface UseYamlValidationResult {
   validationErrors: YamlValidationError[] | null;
   validateMustacheExpressions: (
@@ -138,7 +147,7 @@ export function useYamlValidation({
           const variableContent = match[1].trim();
 
           let errorMessage: string | null = null;
-          let severity: "error" | "warning" = "warning";
+          let severity: YamlValidationErrorSeverity = "warning";
 
           // If we have both the workflow definition and step info, we can do proper validation
           if (
@@ -154,7 +163,7 @@ export function useYamlValidation({
                 : null;
 
             if (currentStep) {
-              const result = validateMustacheVariableNameForYAML(
+              const result = validateMustacheVariableForYAMLStep(
                 variableContent,
                 currentStep,
                 currentStepType,
@@ -166,7 +175,7 @@ export function useYamlValidation({
 
               if (result) {
                 errorMessage = result[0];
-                severity = result[1] as "error" | "warning";
+                severity = result[1] as YamlValidationErrorSeverity;
               }
             }
           } else {
@@ -196,10 +205,7 @@ export function useYamlValidation({
           // Add marker for validation issues
           if (errorMessage) {
             markers.push({
-              severity:
-                severity === "error"
-                  ? MarkerSeverity.Error
-                  : MarkerSeverity.Warning,
+              severity: SEVERITY_MAP[severity],
               message: errorMessage,
               startLineNumber: startPos.lineNumber,
               startColumn: startPos.column,
@@ -233,9 +239,6 @@ export function useYamlValidation({
 
       const errors: YamlValidationError[] = [];
       for (const marker of markers) {
-        if (marker.severity === MarkerSeverity.Hint) {
-          continue;
-        }
         errors.push({
           message: marker.message,
           severity: getSeverityString(marker.severity as MarkerSeverity),
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
index a817d9897d..d660115fc5 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/lib/utils.ts
@@ -20,9 +20,8 @@ export function getSeverityString(
       return "error";
     case MarkerSeverity.Warning:
       return "warning";
-    case MarkerSeverity.Hint:
-      return "hint";
     case MarkerSeverity.Info:
+    case MarkerSeverity.Hint:
     default:
       return "info";
   }
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts b/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
index 178cea86d2..027480b9ed 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/model/types.ts
@@ -1,6 +1,6 @@
 import type { editor } from "monaco-editor";
 
-export type YamlValidationErrorSeverity = "error" | "warning" | "info" | "hint";
+export type YamlValidationErrorSeverity = "error" | "warning" | "info";
 
 export type YamlValidationError = {
   message: string;
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
index f6c0dc0856..6aef035db8 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLEditorStandalone.tsx
@@ -42,7 +42,8 @@ export function WorkflowYAMLEditorStandalone({
     setWorkflowId(workflowId);
   }, [workflowId, setWorkflowId]);
 
-  const isValid = validationErrors?.length === 0;
+  const isValid =
+    validationErrors?.filter((e) => e.severity === "error").length === 0;
 
   const { updateWorkflow } = useWorkflowActions();
   const { data: { providers } = {} } = useProviders();
diff --git a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLValidationErrors.tsx b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLValidationErrors.tsx
index 004803d7ca..ebf2e333bc 100644
--- a/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLValidationErrors.tsx
+++ b/keep-ui/shared/ui/WorkflowYAMLEditor/ui/WorkflowYAMLValidationErrors.tsx
@@ -119,8 +119,11 @@ export function WorkflowYAMLValidationErrors({
             ) : (
               <InformationCircleIcon className="h-4 w-4 text-blue-500 shrink-0 mt-0.5" />
             )}
-            <span className="text-sm">
-              {error.lineNumber}:{error.column} {error.message}
+            <span className="text-sm flex">
+              <span className="opacity-70 min-w-12">
+                {error.lineNumber}:{error.column}
+              </span>
+              <span>{error.message}</span>
             </span>
           </div>
         ))}


Code review comments:
diff_hunk: @@ -70,38 +70,38 @@ describe("validateMustacheVariableName", () => {
   });
 
   it("should prevent accessing current step results", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.First Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.First Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.First Step.results }}' - You can't access the results of the current step."
+      "Variable: 'steps.First Step.results' - You can't access the results of the current step."
     );
   });
 
   it("should prevent accessing future step results", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.Second Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.Second Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.Second Step.results }}' - You can't access the results of a step that appears after the current step."
+      "Variable: 'steps.Second Step.results' - You can't access the results of a step that appears after the current step."
     );
   });
 
   it("should prevent accessing action results from a step", () => {
-    const result = validateMustacheVariableName(
-      "{{ steps.Second Step.results }}",
+    const result = validateMustacheVariableForUIBuilderStep(
+      "steps.Second Step.results",
       mockDefinition.sequence[0],
       mockDefinition,
       mockSecrets
     );
     expect(result).toBe(
-      "Variable: '{{ steps.Second Step.results }}' - You can't access the results of a step that appears after the current step."
+      "Variable: 'steps.Second Step.results' - You can't access the results of a step that appears after the current step."
     );
   });
path: keep-ui/entities/workflows/lib/__tests__/validate-mustache-ui-builder.test.ts
text: @user1:
style: This test case appears to be redundant with the previous 'should prevent accessing future step results' test - they test the same scenario with identical expectations

diff_hunk: @@ -0,0 +1,211 @@
+/**
+ * @fileoverview
+ * Validates a mustache variable name in a UI builder, it's slightly different from the YAML validator because UI builder has a different structure, like nested steps, etc.
+ * TODO: refactor to share code with the YAML validator
+ */
+
+import { V2Step, Definition } from "../model/types";
+import { ALLOWED_MUSTACHE_VARIABLE_REGEX, MUSTACHE_REGEX } from "./mustache";
+
+type V2StepWithParentId = V2Step & { parentId?: string };
+
+/*
+ * Validates a mustache variable name in a UI builder.
+ *
+ * @param cleanedVariableName - Mustache variable name without curly brackets.
+ * @param currentStep - The current step in the sequence in workflow-store format (V2Step + {parentId: string} for loops)
+ * @param definition - The definition of the workflow in workflow-store format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @returns An error message if the variable name is invalid, otherwise null.
+ */
+export const validateMustacheVariableForUIBuilderStep = (
+  cleanedVariableName: string,
+  _currentStep: V2Step,
+  definition: Definition,
+  secrets: Record<string, string>
+): string | null => {
+  const flatSequence = flattenSequence(definition.sequence);
+  const currentStep = flatSequence.find(
+    (step) => step.name === _currentStep.name || step.id === _currentStep.id
+  );
+  if (!currentStep) {
+    // wtf exception, should never happen
+    throw new Error("Current step not found in the sequence");
+  }
+  console.log("flatSequence", flatSequence);
path: keep-ui/entities/workflows/lib/validate-mustache-ui-builder.ts
text: @user1:
style: Remove debug console.log statement before merging to production

```suggestion

```

diff_hunk: @@ -0,0 +1,265 @@
+/**
+ * @fileoverview
+ * Validates a mustache variable name in a YAML workflow definition.
+ * TODO: refactor to share code with the UI builder validator
+ */
+
+import { YamlStepOrAction, YamlWorkflowDefinition } from "../model/yaml.types";
+import { Provider } from "@/shared/api/providers";
+import { ALLOWED_MUSTACHE_VARIABLE_REGEX } from "./mustache";
+import { checkProviderNeedsInstallation } from "./validate-definition";
+
+/**
+ * Validates a mustache variable name in a YAML workflow definition.
+ *
+ * @param cleanedVariableName - Mustache variable name without curly brackets.
+ * @param currentStep - The current step in the sequence in YAML format.
+ * @param currentStepType - The type of the current step.
+ * @param definition - The definition of the workflow in YAML format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @param providers - The providers of the workflow. This is used to validate providers.
+ * @param installedProviders - The installed providers of the workflow. This is used to validate installed providers.
+ * @returns An [error message, "error" | "warning" | "info"] if the variable name is invalid, otherwise null.
+ */
+export const validateMustacheVariableForYAMLStep = (
+  cleanedVariableName: string,
+  currentStep: YamlStepOrAction,
+  currentStepType: "step" | "action",
+  definition: YamlWorkflowDefinition["workflow"],
+  secrets: Record<string, string>,
+  providers: Provider[] | null,
+  installedProviders: Provider[] | null
+): [string, "error" | "warning" | "info"] | null => {
+  if (!cleanedVariableName) {
+    return ["Empty mustache variable.", "warning"];
+  }
+  if (cleanedVariableName === ".") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - short syntax can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (!ALLOWED_MUSTACHE_VARIABLE_REGEX.test(cleanedVariableName)) {
+    if (
+      cleanedVariableName.includes("[") ||
+      cleanedVariableName.includes("]")
+    ) {
+      return [
+        `Variable: '${cleanedVariableName}' - bracket notation is not supported, use dot notation instead.`,
+        "warning",
+      ];
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - contains invalid characters.`,
+      "warning",
+    ];
+  }
+  const parts = cleanedVariableName.split(".");
+  if (!parts.every((part) => part.length > 0)) {
+    return [
+      `Variable: '${cleanedVariableName}' - path parts cannot be empty.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "foreach") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'foreach' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "value") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'value' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "providers") {
+    const providerName = parts[1];
+    if (!providerName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a provider, you need to specify the provider name.`,
+        "warning",
+      ];
+    }
+    if (!providers || !installedProviders) {
+      // Skip validation if providers or installedProviders are not available
+      return null;
+    }
+    const isDefault = providerName.startsWith("default-");
+    if (isDefault) {
+      const providerType = isDefault ? providerName.split("-")[1] : null;
+      const provider = providers.find((p) => p.type === providerType);
+      if (!provider) {
+        return [
+          `Variable: '${cleanedVariableName}' - Unknown provider type '${providerType}'.`,
+          "warning",
+        ];
+      }
+      const doesProviderNeedInstallation =
+        checkProviderNeedsInstallation(provider);
+      const installedProvider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (doesProviderNeedInstallation && !installedProvider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    } else {
+      const provider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (!provider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    }
+    return null;
+  }
+  if (parts[0] === "alert") {
+    // todo: validate alert properties
+    return null;
+  }
+  if (parts[0] === "incident") {
+    // todo: validate incident properties
+    return null;
+  }
+  if (parts[0] === "secrets") {
+    const secretName = parts[1];
+    if (!secretName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a secret, you need to specify the secret name.`,
+        "warning",
+      ];
+    }
+    if (!secrets[secretName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Secret '${secretName}' not found.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "vars") {
+    const varName = parts?.[1];
+    if (!varName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a variable, you need to specify the variable name.`,
+        "warning",
+      ];
+    }
+    if (!currentStep.vars?.[varName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Variable '${varName}' not found in step definition.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "consts") {
+    const constName = parts[1];
+    if (!constName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a constant, you need to specify the constant name.`,
+        "warning",
+      ];
+    }
+    if (!definition.consts?.[constName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Constant '${constName}' not found.`,
+        "error",
+      ];
+    }
+  }
+  if (parts[0] === "steps") {
+    const stepName = parts[1];
+    if (!stepName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access the results of a step, you need to specify the step name.`,
+        "warning",
+      ];
+    }
+    // todo: check if
+    // - the step exists
+    // - it's not the current step (can't access own results, only enrich_alert and enrich_incident can access their own results)
+    // - it's above the current step
+    // - if it's a step it cannot access actions since they run after steps
+    const step = definition.steps?.find((s) => s.name === stepName);
+    const stepIndex =
+      definition.steps?.findIndex((s) => s.name === stepName) ?? -1;
+    const currentStepIndex =
+      currentStepType === "step"
+        ? (definition.steps?.findIndex((s) => s.name === currentStep.name) ??
+          -1)
+        : -1;
+    if (!step || stepIndex === -1) {
+      return [
+        `Variable: '${cleanedVariableName}' - a '${stepName}' step doesn't exist.`,
+        "error",
+      ];
+    }
+    const isCurrentStep = step.name === currentStep.name;
+    if (isCurrentStep) {
+      return [
+        `Variable: '${cleanedVariableName}' - You can't access the results of the current step.`,
+        "error",
+      ];
+    }
+    if (currentStepIndex !== -1 && stepIndex > currentStepIndex) {
+      return [
+        `Variable: '${cleanedVariableName}' - You can't access the results of a step that appears after the current step.`,
+        "error",
+      ];
+    }
+
+    if (!definition.steps?.some((step) => step.name === stepName)) {
+      return [
+        `Variable: '${cleanedVariableName}' - a '${stepName}' step that doesn't exist.`,
+        "error",
+      ];
+    }
+    if (
+      parts.length > 2 &&
+      (parts[2] === "results" ||
+        parts[2].startsWith("results.") ||
+        parts[2].startsWith("results["))
+    ) {
path: keep-ui/entities/workflows/lib/validate-mustache-yaml.ts
text: @user1:
logic: Inconsistent validation - checking for bracket notation in results[...] but bracket notation was forbidden earlier

diff_hunk: @@ -0,0 +1,265 @@
+/**
+ * @fileoverview
+ * Validates a mustache variable name in a YAML workflow definition.
+ * TODO: refactor to share code with the UI builder validator
+ */
+
+import { YamlStepOrAction, YamlWorkflowDefinition } from "../model/yaml.types";
+import { Provider } from "@/shared/api/providers";
+import { ALLOWED_MUSTACHE_VARIABLE_REGEX } from "./mustache";
+import { checkProviderNeedsInstallation } from "./validate-definition";
+
+/**
+ * Validates a mustache variable name in a YAML workflow definition.
+ *
+ * @param cleanedVariableName - Mustache variable name without curly brackets.
+ * @param currentStep - The current step in the sequence in YAML format.
+ * @param currentStepType - The type of the current step.
+ * @param definition - The definition of the workflow in YAML format.
+ * @param secrets - The secrets of the workflow. This is used to validate secrets.
+ * @param providers - The providers of the workflow. This is used to validate providers.
+ * @param installedProviders - The installed providers of the workflow. This is used to validate installed providers.
+ * @returns An [error message, "error" | "warning" | "info"] if the variable name is invalid, otherwise null.
+ */
+export const validateMustacheVariableForYAMLStep = (
+  cleanedVariableName: string,
+  currentStep: YamlStepOrAction,
+  currentStepType: "step" | "action",
+  definition: YamlWorkflowDefinition["workflow"],
+  secrets: Record<string, string>,
+  providers: Provider[] | null,
+  installedProviders: Provider[] | null
+): [string, "error" | "warning" | "info"] | null => {
+  if (!cleanedVariableName) {
+    return ["Empty mustache variable.", "warning"];
+  }
+  if (cleanedVariableName === ".") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - short syntax can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (!ALLOWED_MUSTACHE_VARIABLE_REGEX.test(cleanedVariableName)) {
+    if (
+      cleanedVariableName.includes("[") ||
+      cleanedVariableName.includes("]")
+    ) {
+      return [
+        `Variable: '${cleanedVariableName}' - bracket notation is not supported, use dot notation instead.`,
+        "warning",
+      ];
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - contains invalid characters.`,
+      "warning",
+    ];
+  }
+  const parts = cleanedVariableName.split(".");
+  if (!parts.every((part) => part.length > 0)) {
+    return [
+      `Variable: '${cleanedVariableName}' - path parts cannot be empty.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "foreach") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'foreach' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "value") {
+    if (currentStep.foreach) {
+      return null;
+    }
+    return [
+      `Variable: '${cleanedVariableName}' - 'value' can only be used in a step with foreach.`,
+      "warning",
+    ];
+  }
+  if (parts[0] === "providers") {
+    const providerName = parts[1];
+    if (!providerName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a provider, you need to specify the provider name.`,
+        "warning",
+      ];
+    }
+    if (!providers || !installedProviders) {
+      // Skip validation if providers or installedProviders are not available
+      return null;
+    }
+    const isDefault = providerName.startsWith("default-");
+    if (isDefault) {
+      const providerType = isDefault ? providerName.split("-")[1] : null;
+      const provider = providers.find((p) => p.type === providerType);
+      if (!provider) {
+        return [
+          `Variable: '${cleanedVariableName}' - Unknown provider type '${providerType}'.`,
+          "warning",
+        ];
+      }
+      const doesProviderNeedInstallation =
+        checkProviderNeedsInstallation(provider);
+      const installedProvider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (doesProviderNeedInstallation && !installedProvider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    } else {
+      const provider = installedProviders.find(
+        (p) => p.details.name === providerName
+      );
+      if (!provider) {
+        const providerType = currentStep.provider.type;
+        const availableProvidersOfType = installedProviders.filter(
+          (p) => p.type === providerType
+        );
+        return [
+          `Variable: '${cleanedVariableName}' - Provider '${providerName}' is not installed.${
+            availableProvidersOfType.length > 0
+              ? ` Available '${providerType}' providers: ${availableProvidersOfType.map((p) => p.details.name).join(", ")}`
+              : ""
+          }`,
+          "warning",
+        ];
+      }
+    }
+    return null;
+  }
+  if (parts[0] === "alert") {
+    // todo: validate alert properties
+    return null;
+  }
+  if (parts[0] === "incident") {
+    // todo: validate incident properties
+    return null;
+  }
+  if (parts[0] === "secrets") {
+    const secretName = parts[1];
+    if (!secretName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a secret, you need to specify the secret name.`,
+        "warning",
+      ];
+    }
+    if (!secrets[secretName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Secret '${secretName}' not found.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "vars") {
+    const varName = parts?.[1];
+    if (!varName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a variable, you need to specify the variable name.`,
+        "warning",
+      ];
+    }
+    if (!currentStep.vars?.[varName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Variable '${varName}' not found in step definition.`,
+        "error",
+      ];
+    }
+    return null;
+  }
+  if (parts[0] === "consts") {
+    const constName = parts[1];
+    if (!constName) {
+      return [
+        `Variable: '${cleanedVariableName}' - To access a constant, you need to specify the constant name.`,
+        "warning",
+      ];
+    }
+    if (!definition.consts?.[constName]) {
+      return [
+        `Variable: '${cleanedVariableName}' - Constant '${constName}' not found.`,
+        "error",
+      ];
+    }
+  }
path: keep-ui/entities/workflows/lib/validate-mustache-yaml.ts
text: @user1:
logic: Missing return statement after consts validation - could lead to fall-through to next check

```suggestion
  if (parts[0] === "consts") {
    const constName = parts[1];
    if (!constName) {
      return [
        `Variable: '${cleanedVariableName}' - To access a constant, you need to specify the constant name.`,
        "warning",
      ];
    }
    if (!definition.consts?.[constName]) {
      return [
        `Variable: '${cleanedVariableName}' - Constant '${constName}' not found.`,
        "error",
      ];
    }
    return null;
  }
```

diff_hunk: @@ -16,3 +17,17 @@ export function extractMustacheVariables(yamlString: string): string[] {
       .filter((variable) => variable.length > 0 && !variable.endsWith("."))
   );
 }
+
+/**
+ * Extracts the trimmed value from mustache syntax by removing curly brackets.
+ *
+ * @param mustacheString - A string containing mustache syntax like "{{ variable }}"
+ * @returns The trimmed inner value without curly brackets
+ */
+export function extractMustacheValue(mustacheString: string): string {
+  // Use regex to match content between {{ and }} and trim whitespace
+  const match = mustacheString.match(/\{\{\s*(.*?)\s*\}\}/);
path: keep-ui/entities/workflows/lib/mustache.ts
text: @user1:
style: Using match() with a new regex instance instead of reusing MUSTACHE_REGEX constant. This creates unnecessary duplication

```suggestion
  const match = mustacheString.match(MUSTACHE_REGEX);
```

diff_hunk: @@ -202,12 +202,96 @@ describe("validateAllMustacheVariablesInString", () => {
         consts: {},
       },
     };
-    const result = validateAllMustacheVariablesInString(
+    const result = validateAllMustacheVariablesForUIBuilderStep(
       "keep.dictget({{steps.python-step.results}} , '{{foreach.value.fingerprint}}', 'default') == '{{foreach.value.fingerprint}}'",
       telegramAction, // telegram step
       definition,
       mockSecrets
     );
     expect(result).toEqual([]);
+
+    // short syntax
+    const result2 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ . }}",
+      telegramAction, // telegram step
+      definition,
+      mockSecrets
+    );
+    expect(result2).toEqual([]);
+
+    const result3 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ foreach.value }}",
+      telegramAction,
+      definition,
+      mockSecrets
+    );
+    expect(result3).toEqual([]);
+
+    const result4 = validateAllMustacheVariablesForUIBuilderStep(
+      "{{ value }}",
+      telegramAction,
+      definition,
+      mockSecrets
+    );
+    expect(result4).toEqual([]);
path: keep-ui/entities/workflows/lib/__tests__/validate-mustache-ui-builder.test.ts
text: @user1:
logic: Test case for '{{ value }}' should verify that this only works within a foreach context, as it's a shorthand for foreach.value