diff --git a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/selectors.ts b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/selectors.ts
index 9f4ba7f78c4e7..47c00dc7e93b3 100644
--- a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/selectors.ts
+++ b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/selectors.ts
@@ -51,11 +51,17 @@ export const selectWorkflowDefinition = createSelector(
(computed) => computed?.workflowDefinition
);
-// Only checks if the current workflow yaml can be parses, does check the schema, only the yaml syntax
+// Only checks if the current workflow yaml can be parsed, does not check the schema, only the yaml syntax
export const selectIsYamlSyntaxValid = createSelector(selectYamlComputed, (computed): boolean =>
Boolean(computed?.workflowDefinition)
);
+// Checks whether validation errors (from strict schema + custom validations) are present
+export const selectHasYamlSchemaValidationErrors = createSelector(
+ selectDetail,
+ (detail): boolean => detail.hasYamlSchemaValidationErrors
+);
+
export const selectFocusedStepId = createSelector(selectDetail, (detail) => detail.focusedStepId);
export const selectHighlightedStepId = createSelector(
diff --git a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/slice.ts b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/slice.ts
index 404a98f6e877d..88aad8972d9e8 100644
--- a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/slice.ts
+++ b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/slice.ts
@@ -31,6 +31,7 @@ const initialState: WorkflowDetailState = {
highlightedStepId: undefined,
isTestModalOpen: false,
loading: initialLoadingState,
+ hasYamlSchemaValidationErrors: false,
connectorFlyout: {
isOpen: false,
connectorType: undefined,
@@ -89,6 +90,10 @@ const workflowDetailSlice = createSlice({
state.activeTab = action.payload;
},
+ setHasYamlSchemaValidationErrors: (state, action: { payload: boolean }) => {
+ state.hasYamlSchemaValidationErrors = action.payload;
+ },
+
// Connector flyout actions
openCreateConnectorFlyout: (
state,
@@ -151,6 +156,7 @@ export const {
setExecution,
clearExecution,
setActiveTab,
+ setHasYamlSchemaValidationErrors,
openCreateConnectorFlyout,
openEditConnectorFlyout,
closeConnectorFlyout,
diff --git a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/types.ts b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/types.ts
index cf3fd51b051b9..7312ad9c68666 100644
--- a/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/types.ts
+++ b/src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/types.ts
@@ -49,6 +49,8 @@ export interface WorkflowDetailState {
schema: WorkflowZodSchemaType;
/** Loading states for async operations */
loading: LoadingStates;
+ /** Whether the editor has validation errors (strict schema + custom validations) */
+ hasYamlSchemaValidationErrors: boolean;
/** Connector flyout state */
connectorFlyout: {
isOpen: boolean;
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.test.tsx b/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.test.tsx
index 865421b7eea96..dc019721d3619 100644
--- a/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.test.tsx
+++ b/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.test.tsx
@@ -13,7 +13,8 @@ import type { WorkflowDetailHeaderProps } from './workflow_detail_header';
import { WorkflowDetailHeader } from './workflow_detail_header';
import { createMockStore } from '../../../entities/workflows/store/__mocks__/store.mock';
import {
- _setComputedDataInternal,
+ _clearComputedData,
+ setHasYamlSchemaValidationErrors,
setWorkflow,
setYamlString,
} from '../../../entities/workflows/store/workflow_detail/slice';
@@ -72,27 +73,33 @@ describe('WorkflowDetailHeader', () => {
const renderWithProviders = (
component: React.ReactElement,
- { isValid = true, hasChanges = false }: { isValid?: boolean; hasChanges?: boolean } = {}
+ {
+ isValid = true,
+ hasChanges = false,
+ hasYamlSchemaValidationErrors = false,
+ serverValid = true,
+ }: {
+ isValid?: boolean;
+ hasChanges?: boolean;
+ hasYamlSchemaValidationErrors?: boolean;
+ serverValid?: boolean;
+ } = {}
) => {
const store = createMockStore();
- // Set up the workflow in the store
- store.dispatch(setWorkflow(mockWorkflow));
+ // Set up the workflow in the store (with server-side valid flag)
+ store.dispatch(setWorkflow({ ...mockWorkflow, valid: serverValid }));
store.dispatch(setYamlString(hasChanges ? 'modified yaml' : mockWorkflow.yaml));
- // Set computed data to control syntax validation
- if (isValid) {
- store.dispatch(
- _setComputedDataInternal({
- workflowDefinition: {
- version: '1',
- name: 'Test Workflow',
- enabled: true,
- triggers: [],
- steps: [],
- },
- })
- );
+ if (!isValid) {
+ // Clear the computed data that the middleware auto-generated from the yaml,
+ // so that selectIsYamlSyntaxValid returns false
+ store.dispatch(_clearComputedData());
+ }
+
+ // Simulate strict validation errors from Monaco
+ if (hasYamlSchemaValidationErrors) {
+ store.dispatch(setHasYamlSchemaValidationErrors(true));
}
const wrapper = ({ children }: { children: React.ReactNode }) => {
@@ -158,15 +165,39 @@ describe('WorkflowDetailHeader', () => {
expect(container).toBeTruthy();
});
- // We shouldn't rely on parseResult to determine if the yaml is valid now
- // instead we should move validationErrors to the store and use it to determine it
- it.skip('disables run workflow button when yaml is invalid', () => {
+ it('disables run workflow button when yaml has syntax errors', () => {
const result = renderWithProviders(, {
isValid: false,
});
expect(result.getByTestId('runWorkflowHeaderButton')).toBeDisabled();
});
+ it('disables run workflow button when yaml has validation errors', () => {
+ const result = renderWithProviders(, {
+ isValid: true,
+ hasYamlSchemaValidationErrors: true,
+ });
+ expect(result.getByTestId('runWorkflowHeaderButton')).toBeDisabled();
+ });
+
+ it('disables enabled toggle when yaml has validation errors', () => {
+ const result = renderWithProviders(, {
+ isValid: true,
+ hasYamlSchemaValidationErrors: true,
+ });
+ const toggle = result.getByRole('switch');
+ expect(toggle).toBeDisabled();
+ });
+
+ it('disables enabled toggle when server reports workflow as invalid (e.g. initial page load)', () => {
+ const result = renderWithProviders(, {
+ isValid: true,
+ serverValid: false,
+ });
+ const toggle = result.getByRole('switch');
+ expect(toggle).toBeDisabled();
+ });
+
it('enables run workflow button when yaml is valid', () => {
const result = renderWithProviders();
const button = result.getByTestId('runWorkflowHeaderButton');
diff --git a/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.tsx b/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.tsx
index e4cf63209909b..afb35966b37e2 100644
--- a/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.tsx
+++ b/src/platform/plugins/shared/workflows_management/public/pages/workflow_detail/ui/workflow_detail_header.tsx
@@ -37,6 +37,7 @@ import { useSaveYaml } from '../../../entities/workflows/model/use_save_yaml';
import { useUpdateWorkflow } from '../../../entities/workflows/model/use_update_workflow';
import {
selectHasChanges,
+ selectHasYamlSchemaValidationErrors,
selectIsExecutionsTab,
selectIsSavingYaml,
selectIsYamlSynced,
@@ -105,6 +106,7 @@ export const WorkflowDetailHeader = React.memo(
const workflow = useSelector(selectWorkflow);
const isSyntaxValid = useSelector(selectIsYamlSyntaxValid);
+ const hasYamlSchemaValidationErrors = useSelector(selectHasYamlSchemaValidationErrors);
const hasUnsavedChanges = useSelector(selectHasChanges);
const isExecutionsTab = useSelector(selectIsExecutionsTab);
const isYamlSynced = useSelector(selectIsYamlSynced);
@@ -135,13 +137,17 @@ export const WorkflowDetailHeader = React.memo(
const [showRunConfirmation, setShowRunConfirmation] = useState(false);
+ // Combined validity: syntax must parse AND no strict validation errors AND server considers it valid.
+ // workflow?.valid !== false covers the initial page load before Monaco validates.
+ const isValid = isSyntaxValid && !hasYamlSchemaValidationErrors && workflow?.valid !== false;
+
const runWorkflowTooltipContent = useMemo(() => {
return getTestRunTooltipContent({
isExecutionsTab,
- isValid: isSyntaxValid,
+ isValid,
canRunWorkflow: canExecuteWorkflow,
});
- }, [isSyntaxValid, canExecuteWorkflow, isExecutionsTab]);
+ }, [isValid, canExecuteWorkflow, isExecutionsTab]);
const saveWorkflowTooltipContent = useMemo(() => {
const isCreate = !workflowId;
@@ -262,9 +268,9 @@ export const WorkflowDetailHeader = React.memo(
? i18n.translate('workflows.workflowDetailHeader.unsaved', {
defaultMessage: 'Save changes to enable/disable workflow',
})
- : !isSyntaxValid
+ : !isValid
? i18n.translate('workflows.workflowDetailHeader.invalid', {
- defaultMessage: 'Fix errors to enable workflow',
+ defaultMessage: 'Fix validation errors to enable workflow',
})
: undefined
}
@@ -274,7 +280,7 @@ export const WorkflowDetailHeader = React.memo(
!workflowId ||
isLoading ||
!canUpdateWorkflow ||
- !isSyntaxValid ||
+ !isValid ||
hasUnsavedChanges
}
checked={isEnabled}
@@ -292,7 +298,7 @@ export const WorkflowDetailHeader = React.memo(
iconType="play"
size="s"
onClick={handleRunClickWithUnsavedCheck}
- disabled={isExecutionsTab || !canExecuteWorkflow || isLoading || !isSyntaxValid}
+ disabled={isExecutionsTab || !canExecuteWorkflow || isLoading || !isValid}
aria-label={Translations.runWorkflow}
data-test-subj="runWorkflowHeaderButton"
/>
diff --git a/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.tsx b/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.tsx
index 8aba9baeff69a..b63787a3646e9 100644
--- a/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.tsx
+++ b/src/platform/plugins/shared/workflows_management/public/widgets/workflow_yaml_editor/ui/workflow_yaml_editor.tsx
@@ -53,7 +53,10 @@ import {
selectStepExecutions,
selectWorkflow,
} from '../../../entities/workflows/store/workflow_detail/selectors';
-import { setIsTestModalOpen } from '../../../entities/workflows/store/workflow_detail/slice';
+import {
+ setHasYamlSchemaValidationErrors,
+ setIsTestModalOpen,
+} from '../../../entities/workflows/store/workflow_detail/slice';
import { ActionsMenuPopover } from '../../../features/actions_menu_popover';
import type { ActionOptionData } from '../../../features/actions_menu_popover/types';
import { useMonacoMarkersChangedInterceptor } from '../../../features/validate_workflow_yaml/lib/use_monaco_markers_changed_interceptor';
@@ -256,6 +259,12 @@ export const WorkflowYAMLEditor = ({
workflowYamlSchema: workflowYamlSchema as z.ZodSchema,
});
+ // Sync validation error state to Redux so sibling components (e.g. header toggle) can react
+ useEffect(() => {
+ const hasErrors = validationErrors.some((e) => e.severity === 'error');
+ dispatch(setHasYamlSchemaValidationErrors(hasErrors));
+ }, [validationErrors, dispatch]);
+
const handleErrorClick = useCallback((error: YamlValidationResult) => {
if (!editorRef.current) {
return;