Repository: aws-deadline/deadline-cloud-for-unreal-engine
Pull request #145: feat: Input validation
Author: login: alf-devnull
Created: 2025-05-08T16:08:17Z | Merged: 2025-05-14T18:49:28Z

### What was the problem/requirement? (What/Why)

User Input should be strongly validated to ensure the wrong data will be not submitted to the Farm

### What was the solution? (How)

Warnings in Slate UI on each field in OpenJD entities. Warnings built by OpenJD models rules and restrictions

### What is the impact of this change?

User now can see whats wrong with parameters, names and will be notified if smth is invalid

### How was this change tested?

Unit tests
Smoke tests

### Have you run a render job successfully with these changes?

Yes

### Was this change documented?

Yes

### Is this a breaking change?

No
----

*By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.*


Diff:
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.cpp
index a295c840..692ddaca 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.cpp
@@ -1,47 +1,251 @@
 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 
 #include "DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
 #include "Widgets/Input/SFilePathPicker.h"
 #include "DetailLayoutBuilder.h"
 #include "Widgets/Input/SNumericEntryBox.h"
 #include "EditorDirectories.h"
+#include "Widgets/Notifications/SPopUpErrorText.h"
+#include "DesktopPlatformModule.h"
 
 #define LOCTEXT_NAMESPACE "DeadlineWidgets"
+
 /*
 SDeadlineCloudFilePathWidget is a custom Slate widget class that implements a file path picker interface.
  */
 class  SDeadlineCloudFilePathWidget : public SCompoundWidget
 {
 public:
-    SLATE_BEGIN_ARGS(SDeadlineCloudFilePathWidget) {}
+    SLATE_BEGIN_ARGS(SDeadlineCloudFilePathWidget) 
+		: _BrowseButtonToolTip(LOCTEXT("BrowseButtonToolTip", "Choose a file from this computer"))
+		, _FileTypeFilter(TEXT("All files (*.*)|*.*"))
+		, _Font()
+		, _IsReadOnly(false)
+		, _DialogReturnsFullPath(false)
+		{}
+		/** The property handle for the file path. */
         SLATE_ARGUMENT(TSharedPtr<IPropertyHandle>, PathPropertyHandle)
+
+		/** The function to call when the text is changed. */
+		SLATE_EVENT(FOnVerifyTextChanged, IsValidInput)
+
+		/** Browse button image resource. */
+		SLATE_ATTRIBUTE(const FSlateBrush*, BrowseButtonImage)
+
+		/** Browse button visual style. */
+		SLATE_STYLE_ARGUMENT(FButtonStyle, BrowseButtonStyle)
+
+		/** Browse button tool tip text. */
+		SLATE_ATTRIBUTE(FText, BrowseButtonToolTip)
+
+		/** The directory to browse by default */
+		SLATE_ATTRIBUTE(FString, BrowseDirectory)
+
+		/** Title for the browse dialog window. */
+		SLATE_ATTRIBUTE(FText, BrowseTitle)
+
+		/** The currently selected file path. */
+		SLATE_ATTRIBUTE(FString, FilePath)
+
+		/** File type filter string. */
+		SLATE_ATTRIBUTE(FString, FileTypeFilter)
+
+		/** Font color and opacity of the path text box. */
+		SLATE_ATTRIBUTE(FSlateFontInfo, Font)
+
+		/** Whether the path text box can be modified by the user. */
+		SLATE_ATTRIBUTE(bool, IsReadOnly)
+
+		/** Whether the path returned by the dialog should be converted from relative to full */
+		SLATE_ATTRIBUTE(bool, DialogReturnsFullPath)
+
     SLATE_END_ARGS()
     void Construct(const FArguments& InArgs);
 private:
     TSharedPtr<IPropertyHandle> PathProperty;
+	FOnVerifyTextChanged IsValidInput;
+
+	/** Holds the directory path to browse by default. */
+	TAttribute<FString> BrowseDirectory;
+
+	/** Holds the title for the browse dialog window. */
+	TAttribute<FText> BrowseTitle;
+
+	/** Holds the currently selected file path. */
+	TAttribute<FString> FilePath;
+
+	/** Holds the file type filter string. */
+	TAttribute<FString> FileTypeFilter;
+
+	/** Holds the editable text box. */
+	TSharedPtr<SEditableTextBox> TextBox;
+
+	/** Holds the option for the dialog to return full path instead of relative. */
+	TAttribute<bool> DialogReturnsFullPath;
     FString GetSelectedFilePath() const;
+
+    void OnPathPickedFromDialog(const FString& PickedPath);
     void OnPathPicked(const FString& PickedPath);
+
+	void OnTextChanged(const FText& InText);
+	/** Callback for clicking the browse button. */
+	FReply HandleBrowseButtonClicked( );
+
+	/** Callback for getting the text in the path text box. */
+	FText HandleTextBoxText( ) const;
+
+	/** Callback for committing the text in the path text box. */
+	void HandleTextBoxTextCommitted( const FText& NewText, ETextCommit::Type /*CommitInfo*/ );
+
+	void HandleExternalPathPropertyChanged();
 };
 
 void SDeadlineCloudFilePathWidget::Construct(const FArguments& InArgs)
 {
+	BrowseDirectory = InArgs._BrowseDirectory;
+	BrowseTitle = InArgs._BrowseTitle;
+	FilePath = InArgs._FilePath;
+	FileTypeFilter = InArgs._FileTypeFilter;
+	DialogReturnsFullPath = InArgs._DialogReturnsFullPath;
     PathProperty = InArgs._PathPropertyHandle;
+	IsValidInput = InArgs._IsValidInput;
+
     ChildSlot
         [
-            SNew(SFilePathPicker)
-				.BrowseButtonImage(FAppStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
-				.BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly")
-				.BrowseButtonToolTip(LOCTEXT("FileButtonToolTipText", "Choose a file from this computer"))
-				.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
-				.BrowseTitle(LOCTEXT("PropertyEditorTitle", "File picker..."))
-				.FilePath(this, &SDeadlineCloudFilePathWidget::GetSelectedFilePath)
-				.OnPathPicked(this, &SDeadlineCloudFilePathWidget::OnPathPicked)
+			SNew(SHorizontalBox)
+				+ SHorizontalBox::Slot()
+				.VAlign(VAlign_Fill)
+				.HAlign(HAlign_Fill)
+				.FillWidth(1)
+				[
+					SNew(SHorizontalBox)
+
+					+ SHorizontalBox::Slot()
+						.FillWidth(1.0f)
+						.VAlign(VAlign_Center)
+						[
+							SAssignNew(TextBox, SEditableTextBox)
+								.Text(HandleTextBoxText())
+								.Font(InArgs._Font)
+								.SelectAllTextWhenFocused(true)
+								.ClearKeyboardFocusOnCommit(true)
+								.OnTextCommitted(this, &SDeadlineCloudFilePathWidget::HandleTextBoxTextCommitted)
+								.OnTextChanged(this, &SDeadlineCloudFilePathWidget::OnTextChanged)
+								.OnVerifyTextChanged(IsValidInput)
+								.SelectAllTextOnCommit(false)
+								.IsReadOnly(InArgs._IsReadOnly)
+						]
+
+					+ SHorizontalBox::Slot()
+						.AutoWidth()
+						.Padding(4.0f, 0.0f, 0.0f, 0.0f)
+						.VAlign(VAlign_Center)
+						[
+							SNew(SButton)
+								.ButtonStyle(InArgs._BrowseButtonStyle)
+								.ToolTipText(InArgs._BrowseButtonToolTip)
+								.OnClicked(this, &SDeadlineCloudFilePathWidget::HandleBrowseButtonClicked)
+								.ContentPadding(2.0f)
+								.ForegroundColor(FSlateColor::UseForeground())
+								.IsFocusable(false)
+								[
+									SNew(SImage)
+										.Image(InArgs._BrowseButtonImage)
+										.ColorAndOpacity(FSlateColor::UseForeground())
+								]
+						]
+				]
 		];
+
+	if (IsValidInput.IsBound())
+	{
+		FText OutError = FText::GetEmpty();
+		IsValidInput.Execute(FText::FromString(GetSelectedFilePath()), OutError);
+		TextBox->SetError(OutError);
+	}
+
+	if (PathProperty.IsValid())
+	{
+		PathProperty->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &SDeadlineCloudFilePathWidget::HandleExternalPathPropertyChanged));
+	}
+}
+
+void SDeadlineCloudFilePathWidget::HandleExternalPathPropertyChanged()
+{
+	FString NewPath;
+	if (PathProperty->GetValue(NewPath) == FPropertyAccess::Success)
+	{
+		TextBox->SetText(FText::FromString(NewPath));
+	}
+}
+
+FReply SDeadlineCloudFilePathWidget::HandleBrowseButtonClicked()
+{
+	IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
+
+	if (DesktopPlatform == nullptr)
+	{
+		return FReply::Handled();
+	}
+
+	const FString DefaultPath = BrowseDirectory.IsSet()
+		? BrowseDirectory.Get()
+		: FPaths::GetPath(GetSelectedFilePath());
+
+	TSharedPtr<SWindow> ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared());
+	void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid())
+		? ParentWindow->GetNativeWindow()->GetOSWindowHandle()
+		: nullptr;
+
+	TArray<FString> OutFiles;
+
+	if (DesktopPlatform->OpenFileDialog(ParentWindowHandle, BrowseTitle.Get().ToString(), DefaultPath, TEXT(""), FileTypeFilter.Get(), EFileDialogFlags::None, OutFiles))
+	{
+		if (DialogReturnsFullPath.Get())
+		{
+			OnPathPickedFromDialog(FPaths::ConvertRelativePathToFull(OutFiles[0]));
+		}
+		else
+		{
+			OnPathPickedFromDialog(OutFiles[0]);
+		}
+	}
+
+	return FReply::Handled();
+}
+
+FText SDeadlineCloudFilePathWidget::HandleTextBoxText() const
+{
+	return FText::FromString(GetSelectedFilePath());
+}
+
+void SDeadlineCloudFilePathWidget::OnTextChanged(const FText& InText)
+{
+	TextBox->SetText(InText);
+}
+
+void SDeadlineCloudFilePathWidget::HandleTextBoxTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo)
+{
+	OnPathPicked(NewText.ToString());
+}
+
+void SDeadlineCloudFilePathWidget::OnPathPickedFromDialog(const FString& PickedPath)
+{
+	if (IsValidInput.IsBound())
+	{
+		FText Error = FText::GetEmpty();
+		IsValidInput.Execute(FText::FromString(PickedPath), Error);
+		TextBox->SetError(Error);
+	}
+
+	OnPathPicked(PickedPath);
 }
 
 void SDeadlineCloudFilePathWidget::OnPathPicked(const FString& PickedPath)
 {
 	FPropertyAccess::Result PathResult = PathProperty->SetValue(PickedPath);
+
 	if (PathResult != FPropertyAccess::Success)
 	{
 		UE_LOG(LogTemp, Error, TEXT("SetValue failed! Result: %d"), static_cast<int32>(PathResult));
@@ -50,11 +254,12 @@ void SDeadlineCloudFilePathWidget::OnPathPicked(const FString& PickedPath)
 
 FString SDeadlineCloudFilePathWidget::GetSelectedFilePath() const
 {
-	FString FilePath;
-	PathProperty->GetValue(FilePath);
+	FString PropertyFilePath;
+	PathProperty->GetValue(PropertyFilePath);
 
-	return FilePath;
+	return PropertyFilePath;
 }
+
 /*
 SDeadlineCloudStringWidget is a custom Slate widget that creates an editable text box for string properties.
 It handles the display and editing of string values through a property handle.
@@ -64,12 +269,13 @@ class SDeadlineCloudStringWidget : public SCompoundWidget
 public:
 	SLATE_BEGIN_ARGS(SDeadlineCloudStringWidget) {}
 		SLATE_ARGUMENT(TSharedPtr<IPropertyHandle>, StringPropertyHandle)
+		SLATE_EVENT(FOnVerifyTextChanged, IsValidInput)
 	SLATE_END_ARGS()
 
 	void Construct(const FArguments& InArgs)
 	{
 		StringProperty = InArgs._StringPropertyHandle;
-
+		IsValidInput = InArgs._IsValidInput;
 		ChildSlot
 			[
 				SNew(SHorizontalBox)
@@ -77,18 +283,32 @@ class SDeadlineCloudStringWidget : public SCompoundWidget
 					.FillWidth(1.0f)
 					.VAlign(VAlign_Center)
 					[
-						SNew(SEditableTextBox)
+						SAssignNew(TextBox, SEditableTextBox)
 							.Font(IDetailLayoutBuilder::GetDetailFont())
-							.OnTextChanged(this, &SDeadlineCloudStringWidget::HandleTextChanged)
 							.Text(this, &SDeadlineCloudStringWidget::GetText)
+							.OnTextCommitted(this, &SDeadlineCloudStringWidget::OnTextCommitted)
+							.OnTextChanged(this, &SDeadlineCloudStringWidget::OnTextChanged)
+							.OnVerifyTextChanged(IsValidInput)
 					]
 			];
+
+		if (IsValidInput.IsBound())
+		{
+			Error = FText::GetEmpty();
+			IsValidInput.Execute(GetText(), Error);
+			TextBox->SetError(Error);
+		}
 	}
 
 private:
-	void HandleTextChanged(const FText& NewText)
+
+	void OnTextChanged(const FText& InText)
 	{
-		StringProperty->SetValue(NewText.ToString());
+	}
+
+	void OnTextCommitted(const FText& InText, ETextCommit::Type InCommitType)
+	{
+		StringProperty->SetValue(InText.ToString());
 	}
 
 	FText GetText() const
@@ -100,6 +320,9 @@ class SDeadlineCloudStringWidget : public SCompoundWidget
 	}
 
 	TSharedPtr<IPropertyHandle> StringProperty;
+	TSharedPtr<SEditableTextBox> TextBox;
+	FOnVerifyTextChanged IsValidInput;
+	FText Error;
 };
 /*
 SDeadlineCloudIntWidget is a custom Slate widget for integer input fields.
@@ -250,18 +473,20 @@ void FDeadlineCloudDetailsWidgetsHelper::SEyeUpdateWidget::Construct(const FArgu
 };
 
 
-TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(TSharedPtr<IPropertyHandle> ParameterHandle, EValueType Type)
+TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(TSharedPtr<IPropertyHandle> ParameterHandle, EValueType Type, EValueValidationType ValidationType)
 {
 	switch (Type)
 	{
 		using enum EValueType;
 	case EValueType::STRING:
 	{
-		return CreateStringWidget(ParameterHandle);
+		FOnVerifyTextChanged Validation = FDeadlineCloudInputValidationHelper::GetStringValidationFunction(ValidationType);
+		return CreateStringWidget(ParameterHandle, Validation);
 	}
 	case EValueType::PATH:
 	{
-		return CreatePathWidget(ParameterHandle);
+		FOnVerifyTextChanged Validation = FDeadlineCloudInputValidationHelper::GetPathValidationFunction(ValidationType);
+		return CreatePathWidget(ParameterHandle, Validation);
 	}
 	case EValueType::INT:
 	{
@@ -281,6 +506,50 @@ TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByTy
 	return SNullWidget::NullWidget;
 }
 
+TSharedPtr<SWidget> FDeadlineCloudDetailsWidgetsHelper::TryCreatePropertyWidgetFromMetadata(TSharedPtr<IPropertyHandle> ParameterHandle)
+{
+	if (!ParameterHandle.IsValid())
+	{
+		return nullptr;
+	}
+
+	FString TypeString;
+	FString ValidationTypeString;
+
+    if (const FString* CustomWidget = ParameterHandle->GetProperty()->FindMetaData(TEXT("CustomWidgetType")))
+    {
+		FString CustomWidgetName(*CustomWidget);
+        UEnum* EnumPtr = StaticEnum<EValueType>();
+		if (EnumPtr)
+		{
+			const int32 EnumValue = EnumPtr->GetValueByName(FName(*CustomWidgetName));
+			if (EnumValue != INDEX_NONE)
+			{
+				EValueValidationType ValidationType = EValueValidationType::Default;
+
+				if (const FString* Validation = ParameterHandle->GetProperty()->FindMetaData(TEXT("ValidationType")))
+				{
+					FString ValidationString(*Validation);
+					UEnum* EnumValueType = StaticEnum<EValueValidationType>();
+                    if (EnumValueType)
+					{
+						const int32 EnumValidationValue = EnumValueType->GetValueByName(FName(*ValidationString));
+						if (EnumValidationValue != INDEX_NONE)
+                        {
+							ValidationType = EValueValidationType(EnumValidationValue);
+						}
+					}
+				}
+
+				return FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(
+					ParameterHandle, EValueType(EnumValue), ValidationType);
+			}
+		}
+    }
+
+	return nullptr;
+}
+
 TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateNameWidget(FString Parameter)
 {
 	return  SNew(SHorizontalBox)
@@ -310,10 +579,16 @@ TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateEyeUpdateWidget()
 	return  EyeUpdateWidget;
 }
 
-TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreatePathWidget(TSharedPtr<IPropertyHandle> ParameterHandle)
+TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreatePathWidget(TSharedPtr<IPropertyHandle> ParameterHandle, FOnVerifyTextChanged Validation)
 {
 	return SNew(SDeadlineCloudFilePathWidget)
-		.PathPropertyHandle(ParameterHandle);
+		.PathPropertyHandle(ParameterHandle)
+		.IsValidInput(Validation)
+		.BrowseButtonImage(FAppStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
+		.BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly")
+		.BrowseButtonToolTip(LOCTEXT("FileButtonToolTipText", "Choose a file from this computer"))
+		.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
+		.BrowseTitle(LOCTEXT("PropertyEditorTitle", "File picker..."));
 }
 
 TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateIntWidget(TSharedPtr<IPropertyHandle> ParameterHandle)
@@ -328,10 +603,11 @@ TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateFloatWidget(TShare
 		.PropertyHandle(ParameterHandle);
 }
 
-TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateStringWidget(TSharedPtr<IPropertyHandle> ParameterHandle)
+TSharedRef<SWidget> FDeadlineCloudDetailsWidgetsHelper::CreateStringWidget(TSharedPtr<IPropertyHandle> ParameterHandle, FOnVerifyTextChanged Validation)
 {
 	return SNew(SDeadlineCloudStringWidget)
-		.StringPropertyHandle(ParameterHandle);
+		.StringPropertyHandle(ParameterHandle)
+		.IsValidInput(Validation);
 }
 
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudEnvironmentDetails.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudEnvironmentDetails.cpp
index bc379388..fa4a9684 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudEnvironmentDetails.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudEnvironmentDetails.cpp
@@ -20,6 +20,7 @@
 #include "Widgets/Input/SFilePathPicker.h"
 #include "Widgets/Input/SEditableTextBox.h"
 
+#include "Framework/MetaData/DriverMetaData.h"
 #define LOCTEXT_NAMESPACE "EnvironmentDetails"
 
 bool FDeadlineCloudEnvironmentDetails::CheckConsistency(UDeadlineCloudEnvironment* Env)
@@ -63,6 +64,29 @@ void FDeadlineCloudEnvironmentDetails::CustomizeDetails(IDetailLayoutBuilder& De
 		bCheckConsistensyPassed = CheckConsistency(MyObject);
 	}
 
+	TSharedRef<IPropertyHandle> PathToTemplate = MainDetailLayout->GetProperty("PathToTemplate");
+	IDetailPropertyRow* PathToTemplateRow = MainDetailLayout->EditDefaultProperty(PathToTemplate);
+
+	if (PathToTemplateRow)
+	{
+		TSharedPtr<SWidget> NameWidget;
+		TSharedPtr<SWidget> ValueWidget;
+		PathToTemplateRow->GetDefaultWidgets(NameWidget, ValueWidget);
+
+		FName Tag = FName("Environment.PathToTemplate");
+		ValueWidget->AddMetadata(FDriverMetaData::Id(Tag));
+
+		PathToTemplateRow->CustomWidget()
+			.NameContent()
+			[
+				NameWidget.ToSharedRef()
+			]
+			.ValueContent()
+			[
+				ValueWidget.ToSharedRef()
+			];	
+	}
+
 	IDetailCategoryBuilder& PropertiesCategory = MainDetailLayout->EditCategory("Parameters");
 
 	PropertiesCategory.AddCustomRow(FText::FromString("Consistency"))
@@ -140,6 +164,13 @@ void FDeadlineCloudEnvironmentParametersMapBuilder::GenerateChildContent(IDetail
 		TSharedPtr<SWidget> NameWidget;
 		TSharedPtr<SWidget> ValueWidget;
 
+		TSharedPtr<SWidget> CustomValueWidget = FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(ItemHandle, EValueType::STRING, EValueValidationType::EnvParameterValue);
+		TSharedPtr<IPropertyHandle> KeyHandle = ItemHandle->GetKeyHandle();
+		FString Name;
+		KeyHandle->GetValue(Name);
+		FName Tag = FName("EnvironmentParameter." + Name);
+		CustomValueWidget->AddMetadata(FDriverMetaData::Id(Tag));
+
 		ItemRow.GetDefaultWidgets(NameWidget, ValueWidget);
 		ItemRow.CustomWidget(true)
 			.CopyAction(EmptyCopyPasteAction)
@@ -159,7 +190,7 @@ void FDeadlineCloudEnvironmentParametersMapBuilder::GenerateChildContent(IDetail
 					.FillWidth(1.0f)
 					.Padding(2.0f, 0.0f)
 					[
-						ValueWidget.ToSharedRef()
+						CustomValueWidget.ToSharedRef()
 					]
 			];
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp
new file mode 100644
index 00000000..72beb414
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp
@@ -0,0 +1,180 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
+
+#define LOCTEXT_NAMESPACE "DeadlineWidgets"
+
+
+//TODO: Refactor this class to use OpenJD validators
+//The best approach to validation is to create a wrapper around the OpenJD validators to avoid 
+//code duplication and reduce the chance of errors. However, for now, private functions are used internally
+
+static FOnVerifyTextChanged MakeValidator(FValidatorFunc Func)
+{
+    return FOnVerifyTextChanged::CreateLambda(
+        [Func](const FText& Input, FText& Error) -> bool
+        {
+            const FString InputString = Input.ToString();
+            return Func(InputString, Error);
+        });
+}
+
+FValidatorFunc FDeadlineCloudInputValidationHelper::CreateLengthValidator(int32 Min, int32 Max)
+{
+    return [Min, Max](const FString& Str, FText& Error)
+    {
+        return IsValidLength(Str, Min, Max, Error);
+    };
+}
+
+FValidatorFunc FDeadlineCloudInputValidationHelper::CreateLengthAndIdentifierValidator(int32 Min, int32 Max)
+{
+    return [Min, Max](const FString& Str, FText& Error)
+    {
+        return IsValidLength(Str, Min, Max, Error)
+            && IsValidIdentifier(Str, Error);
+    };
+}
+
+FValidatorFunc FDeadlineCloudInputValidationHelper::CreateLengthAndControlValidator(int32 Min, int32 Max, TSet<TCHAR> ExcludeList)
+{
+    return [Min, Max, ExcludeList](const FString& Str, FText& Error)
+    {
+        return IsValidLength(Str, Min, Max, Error)
+            && ContainsNoControlCharacters(Str, Error, ExcludeList);
+    };
+}
+
+FOnVerifyTextChanged FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType ValidationType)
+{
+    using enum EValueValidationType;
+
+    switch (ValidationType)
+    {
+    case JobName:
+        return MakeValidator(CreateLengthAndIdentifierValidator(1, 64));
+
+    case JobDescription:
+        return MakeValidator(CreateLengthAndControlValidator(0, 2048, { '\n', '\r', '\t' }));
+
+    case JobParameterValue:
+        return MakeValidator(CreateLengthValidator(0, 1024));
+
+    case StepParameterValue:
+        return MakeValidator(CreateLengthValidator(1, 1024));
+
+    case EnvParameterValue:
+        return MakeValidator(CreateLengthValidator(0, 2048));
+
+    default:
+        return FOnVerifyTextChanged();
+    }
+}
+
+FOnVerifyTextChanged FDeadlineCloudInputValidationHelper::GetPathValidationFunction(EValueValidationType ValidationType)
+{
+    using enum EValueValidationType;
+
+    switch (ValidationType)
+    {
+    case JobParameterValue:
+        return MakeValidator(CreateLengthValidator(0, 1024));
+
+    case StepParameterValue:
+        return MakeValidator(CreateLengthValidator(1, 1024));
+
+    default:
+        return FOnVerifyTextChanged();
+    }
+}
+
+bool FDeadlineCloudInputValidationHelper::IsValidLength(const FString& InStr, int32 Min, int32 Max)
+{
+    const int32 Length = InStr.Len();
+    return Length >= Min && Length <= Max;
+}
+
+bool FDeadlineCloudInputValidationHelper::IsValidLength(const FString& InStr, int32 Min, int32 Max, FText& OutError, const FText& FieldName)
+{
+    if (!IsValidLength(InStr, Min, Max))
+    {
+        OutError = FText::Format(
+            LOCTEXT("InvalidLength", "{0} length must be between {1} and {2} characters."),
+            FieldName,
+            FText::AsNumber(Min),
+            FText::AsNumber(Max)
+        );
+
+        return false;
+    }
+
+    return true;
+}
+
+bool FDeadlineCloudInputValidationHelper::ContainsNoControlCharacters(const FString& InStr, const TSet<TCHAR>& ExcludeList)
+{
+    for (const TCHAR Ch : InStr)
+    {
+        if (FChar::IsControl(Ch) && !ExcludeList.Contains(Ch))
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool FDeadlineCloudInputValidationHelper::ContainsNoControlCharacters(const FString& InStr, FText& OutError, const TSet<TCHAR>& ExcludeList, const FText& FieldName)
+{
+    if (!ContainsNoControlCharacters(InStr, ExcludeList))
+    {
+        OutError = FText::Format(
+            LOCTEXT("InvalidControlChars", "{0} contains invalid control characters."),
+            FieldName
+        );
+
+        return false;
+    }
+
+    return true;
+}
+
+bool FDeadlineCloudInputValidationHelper::IsValidIdentifier(const FString& InStr)
+{
+    if (InStr.IsEmpty())
+    {
+        return false;
+    }
+
+    if (!(FChar::IsAlpha(InStr[0]) || InStr[0] == TEXT('_')))
+    {
+        return false;
+    }
+
+    for (const TCHAR Ch : InStr)
+    {
+        if (!(FChar::IsAlpha(Ch) || FChar::IsDigit(Ch) || Ch == TEXT('_')))
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool FDeadlineCloudInputValidationHelper::IsValidIdentifier(const FString& InStr, FText& OutError, const FText& FieldName)
+{
+    if (!IsValidIdentifier(InStr))
+    {
+        OutError = FText::Format(
+            LOCTEXT("InvalidIdentifier", "{0} must start with a letter or underscore and contain only Latin letters, digits, or underscores."),
+            FieldName
+        );
+
+        return false;
+    }
+
+    return true;
+}
+
+#undef LOCTEXT_NAMESPACE
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobDetails.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobDetails.cpp
index b7db1c61..3a037e5a 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobDetails.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobDetails.cpp
@@ -26,6 +26,8 @@
 #include "MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.h"
 #include "DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h"
 
+#include "Framework/MetaData/DriverMetaData.h"
+
 #define LOCTEXT_NAMESPACE "JobDetails"
 
 
@@ -501,7 +503,9 @@ void FDeadlineCloudJobParametersArrayBuilder::OnGenerateEntry(TSharedRef<IProper
     TSharedPtr<SWidget> ValueWidget;
 
     PropertyRow.GetDefaultWidgets(NameWidget, ValueWidget);
-    ValueWidget = FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(ValueHandle, Type);
+    ValueWidget = FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(ValueHandle, Type, EValueValidationType::JobParameterValue);
+	FName Tag = FName("JobParameter." + ParameterName);
+	ValueWidget->AddMetadata(FDriverMetaData::Id(Tag));
 
     bool Checked = !(IsEyeWidgetEnabled(FName(ParameterName)));
     TSharedRef<FDeadlineCloudDetailsWidgetsHelper::SEyeCheckBox> EyeWidget = SNew(FDeadlineCloudDetailsWidgetsHelper::SEyeCheckBox, FName(ParameterName), Checked);
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.cpp
index 4fdd5d6a..f7ba2678 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.cpp
@@ -4,11 +4,14 @@
 #include "MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.h"
 #include "DetailWidgetRow.h"
 #include "IDetailChildrenBuilder.h"
+#include "DetailLayoutBuilder.h"
 #include "IDetailGroup.h"
 #include "PropertyCustomizationHelpers.h"
 #include "Widgets/Input/SCheckBox.h"
 #include "Misc/EngineVersionComparison.h"
 #include "DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h"
+#include "Framework/MetaData/DriverMetaData.h"
+#define LOCTEXT_NAMESPACE "UnrealDeadlineCloudServiceModule"
 
 TSharedRef<IPropertyTypeCustomization> FDeadlineCloudJobPresetDetailsCustomization::MakeInstance()
 {
@@ -76,13 +79,21 @@ void FDeadlineCloudJobPresetDetailsCustomization::CustomizeChildren(TSharedRef<I
 
         IDetailPropertyRow& PropertyRow = GroupToUse->AddPropertyRow(ChildHandle);
 
+        TSharedPtr<SWidget> CustomValueWidget = FDeadlineCloudDetailsWidgetsHelper::TryCreatePropertyWidgetFromMetadata(ChildHandle);
+        if (CustomValueWidget.IsValid())
+        {
+            FString ParameterName = ChildHandle->GetProperty()->GetName();
+            FName Tag = FName("JobPreset." + ParameterName);
+            CustomValueWidget->AddMetadata(FDriverMetaData::Id(Tag));
+        }
+
         if (OuterJob)
         {
-            CustomizeStructChildrenInMovieRenderQueue(PropertyRow, OuterJob);
+            CustomizeStructChildrenInMovieRenderQueue(PropertyRow, OuterJob, CustomValueWidget);
         }
         else
         {
-            CustomizeStructChildrenInAssetDetails(PropertyRow);
+            CustomizeStructChildrenInAssetDetails(PropertyRow, CustomValueWidget);
         }
     }
 
@@ -97,7 +108,7 @@ void FDeadlineCloudJobPresetDetailsCustomization::CustomizeChildren(TSharedRef<I
 }
 
 void FDeadlineCloudJobPresetDetailsCustomization::CustomizeStructChildrenInAssetDetails(
-    IDetailPropertyRow& PropertyRow) const
+    IDetailPropertyRow& PropertyRow, TSharedPtr<SWidget> CustomValueWidget) const
 {
     TSharedPtr<SWidget> NameWidget;
     TSharedPtr<SWidget> ValueWidget;
@@ -117,14 +128,14 @@ void FDeadlineCloudJobPresetDetailsCustomization::CustomizeStructChildrenInAsset
         .MaxDesiredWidth(Row.ValueWidget.MaxWidth)
         .VAlign(VAlign_Center)
         [
-            ValueWidget.ToSharedRef()
+            CustomValueWidget.IsValid() ? CustomValueWidget.ToSharedRef() : ValueWidget.ToSharedRef()
         ];
 }
 
 void FDeadlineCloudJobPresetDetailsCustomization::CustomizeStructChildrenInMovieRenderQueue(
-    IDetailPropertyRow& PropertyRow, UMoviePipelineDeadlineCloudExecutorJob* Job) const
+    IDetailPropertyRow& PropertyRow, UMoviePipelineDeadlineCloudExecutorJob* Job, TSharedPtr<SWidget> CustomValueWidget) const
 {
-    PropertyOverrideHandler->EnableInMovieRenderQueue(PropertyRow);
+    PropertyOverrideHandler->EnableInMovieRenderQueue(PropertyRow, CustomValueWidget);
 }
 
 TSharedRef<IPropertyTypeCustomization> FDeadlineCloudAttachmentDetailsCustomization::MakeInstance()
@@ -166,7 +177,6 @@ void FDeadlineCloudAttachmentDetailsCustomization::CustomizeChildren(
     UMoviePipelineDeadlineCloudExecutorJob* OuterJob = FPropertyAvailabilityHandler::GetOuterJob(StructHandle);
     PropertyOverrideHandler = MakeShared<FPropertyAvailabilityHandler>(OuterJob);
 
-
     if (OuterJob)
     {
         PropertyOverrideHandler->EnableInMovieRenderQueue(PathsRow);
@@ -184,6 +194,85 @@ void FDeadlineCloudAttachmentDetailsCustomization::CustomizeChildren(
     else
         PropertyOverrideHandler->DisableRowInDataAsset(AutoDetectedPathsRow);
 
+    TSharedPtr<SWidget> PathsNameWidget;
+    TSharedPtr<SWidget> PathsValueWidget;
+    PathsRow.GetDefaultWidgets(PathsNameWidget, PathsValueWidget);
+    PathsRow.CustomWidget(true)
+        .NameContent()
+        [
+            PathsNameWidget.ToSharedRef()
+        ]
+        .ValueContent()
+        [
+            SNew(SHorizontalBox)
+                + SHorizontalBox::Slot()
+                .HAlign(HAlign_Left)
+                .VAlign(VAlign_Center)
+                .Padding(4, 0)
+                [
+                    SNew(STextBlock)
+                        .Text(LOCTEXT("PathsError", "Contains empty elements or more than 50 of them"))
+                        .Font(IDetailLayoutBuilder::GetDetailFont())
+                        .ColorAndOpacity(FLinearColor::Red)
+                        .Visibility(TAttribute<EVisibility>::Create(
+                            TAttribute<EVisibility>::FGetter::CreateSPLambda(
+                                this, [this, PathsHandle]() {
+                                    return GetPathsErrorWidgetVisibility(PathsHandle);
+                                })))
+                ]
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .HAlign(HAlign_Left)
+                .VAlign(VAlign_Center)
+                [
+                    SNew(SOverlay)
+                        + SOverlay::Slot()
+                        [
+                            PathsValueWidget.ToSharedRef()
+                        ]
+                ]
+        ];
+
+    TSharedPtr<SWidget> AutoDetectedPathsNameWidget;
+    TSharedPtr<SWidget> AutoDetectedPathsValueWidget;
+    AutoDetectedPathsRow.GetDefaultWidgets(AutoDetectedPathsNameWidget, AutoDetectedPathsValueWidget);
+    AutoDetectedPathsRow.CustomWidget(true)
+        .NameContent()
+        [
+            AutoDetectedPathsNameWidget.ToSharedRef()
+        ]
+        .ValueContent()
+        [
+            SNew(SHorizontalBox)
+                + SHorizontalBox::Slot()
+                .HAlign(HAlign_Left)
+                .VAlign(VAlign_Center)
+                .Padding(4, 0)
+                [
+                    SNew(STextBlock)
+                        .Text(LOCTEXT("AutoDetectedPathsError", "Contains empty elements or more than 50 of them"))
+                        .Font(IDetailLayoutBuilder::GetDetailFont())
+                        .ColorAndOpacity(FLinearColor::Red)
+                        .Visibility(TAttribute<EVisibility>::Create(
+                            TAttribute<EVisibility>::FGetter::CreateSPLambda(
+                                this, [this, AutoDetectedPathsHandle]() {
+                                    return GetPathsErrorWidgetVisibility(AutoDetectedPathsHandle);
+                                })))
+                ]
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .HAlign(HAlign_Left)
+                .VAlign(VAlign_Center)
+                [
+                    SNew(SOverlay)
+                        + SOverlay::Slot()
+                        [
+                            AutoDetectedPathsValueWidget.ToSharedRef()
+                        ]
+                ]
+        ];
+
+
     // Since we updating auto-detected files mostly to show them in the UI. We don't want to put it into job initialization methods
     if (OuterJob && StructHandle->GetProperty()->GetName() == "InputFiles")
     {
@@ -191,12 +280,78 @@ void FDeadlineCloudAttachmentDetailsCustomization::CustomizeChildren(
     }
 }
 
-bool FDeadlineCloudJobPresetDetailsCustomization::IsPropertyHiddenInMovieRenderQueue(const FName& InPropertyPath)
+bool FDeadlineCloudAttachmentDetailsCustomization::IsPathsContainsErrors(TSharedPtr<IPropertyHandle> PropertyHandle) const
 {
+	if (!PropertyHandle.IsValid())
+	{
+		return false;
+	}
+	auto Paths = PropertyHandle->GetChildHandle("Paths", false);
+	if (!Paths.IsValid())
+	{
+		return false;
+	}
+
+	auto ArrayHandle = Paths->AsArray();
+	if (!ArrayHandle.IsValid())
+	{
+		return false;
+	}
+
+	uint32 NumChildren = 0;
+	ArrayHandle->GetNumElements(NumChildren);
+    if (NumChildren >= 50)
+    {
+        return true;
+    }
+
+    for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
+    {
+        TSharedPtr<IPropertyHandle> ChildHandle = ArrayHandle->GetElement(ChildIndex);
+        if (!ChildHandle.IsValid())
+        {
+            continue;
+        }
+
+        FString Value;
+        auto FilePath = ChildHandle->GetChildHandle("FilePath", false);
+        if (FilePath.IsValid())
+        {
+            FilePath->GetValue(Value);
+        }
+        else
+        {
+			auto Path = ChildHandle->GetChildHandle("Path", false);
+			if (Path.IsValid())
+			{
+				Path->GetValue(Value);
+			}
+        }
+
+        if (Value.IsEmpty())
+        {
+            return true;
+        }
+    }
 
     return false;
 }
 
+EVisibility FDeadlineCloudAttachmentDetailsCustomization::GetPathsErrorWidgetVisibility(TSharedPtr<IPropertyHandle> PropertyHandle) const
+{
+    return IsPathsContainsErrors(PropertyHandle) ? EVisibility::Visible : EVisibility::Collapsed;
+}
+
+EVisibility FDeadlineCloudAttachmentDetailsCustomization::GetPathsDefaultWidgetVisibility(TSharedPtr<IPropertyHandle> PropertyHandle) const
+{
+    return IsPathsContainsErrors(PropertyHandle) ? EVisibility::Collapsed : EVisibility::Visible;
+}
+
+bool FDeadlineCloudJobPresetDetailsCustomization::IsPropertyHiddenInMovieRenderQueue(const FName& InPropertyPath)
+{
+    return false;
+}
+
 bool GetPresetValueAsString(const FProperty* PropertyPtr, UMoviePipelineDeadlineCloudExecutorJob* Job, FString& OutFormattedValue)
 {
     if (!PropertyPtr || !Job)
@@ -386,7 +541,7 @@ void FPropertyAvailabilityHandler::DisableRowInDataAsset(const IDetailPropertyRo
 }
 
 
-void FPropertyAvailabilityHandler::EnableInMovieRenderQueue(IDetailPropertyRow& PropertyRow) const
+void FPropertyAvailabilityHandler::EnableInMovieRenderQueue(IDetailPropertyRow& PropertyRow, TSharedPtr<SWidget> CustomValueWidget) const
 {
     if (!Job) return;
 
@@ -396,13 +551,19 @@ void FPropertyAvailabilityHandler::EnableInMovieRenderQueue(IDetailPropertyRow&
     PropertyRow.GetDefaultWidgets(NameWidget, ValueWidget, Row);
 
     const FName PropertyPath = *PropertyRow.GetPropertyHandle()->GetProperty()->GetPathName();
-    ValueWidget->SetEnabled(
-        TAttribute<bool>::CreateLambda([this, PropertyPath]()
-            {
-                return Job->IsPropertyRowEnabledInMovieRenderJob(PropertyPath);
-            }
-        )
-    );
+    TAttribute<bool> IsEnabled = TAttribute<bool>::CreateLambda([this, PropertyPath]()
+        {
+            return Job->IsPropertyRowEnabledInMovieRenderJob(PropertyPath);
+        });
+
+    if (CustomValueWidget.IsValid())
+    {
+        CustomValueWidget->SetEnabled(IsEnabled);
+    }
+	else
+	{
+		ValueWidget->SetEnabled(IsEnabled);
+	}
 
     PropertyRow
         .CustomWidget(true)
@@ -439,6 +600,8 @@ void FPropertyAvailabilityHandler::EnableInMovieRenderQueue(IDetailPropertyRow&
         .MaxDesiredWidth(Row.ValueWidget.MaxWidth)
         .VAlign(VAlign_Center)
         [
-            ValueWidget.ToSharedRef()
+            CustomValueWidget.IsValid() ? CustomValueWidget.ToSharedRef() : ValueWidget.ToSharedRef()
         ];
 }
+
+#undef LOCTEXT_NAMESPACE
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudStepDetails.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudStepDetails.cpp
index 4deb728d..56189781 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudStepDetails.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudStepDetails.cpp
@@ -21,6 +21,7 @@
 #include "MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.h"
 #include "DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h"
 
+#include "Framework/MetaData/DriverMetaData.h"
 #define LOCTEXT_NAMESPACE "StepDetails"
 
 
@@ -71,6 +72,29 @@ void FDeadlineCloudStepDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBui
     DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized);
 	Settings = Cast<UDeadlineCloudStep>(ObjectsBeingCustomized[0].Get());
 
+	TSharedRef<IPropertyHandle> PathToTemplate = MainDetailLayout->GetProperty("PathToTemplate");
+	IDetailPropertyRow* PathToTemplateRow = MainDetailLayout->EditDefaultProperty(PathToTemplate);
+
+	if (PathToTemplateRow)
+	{
+		TSharedPtr<SWidget> NameWidget;
+		TSharedPtr<SWidget> ValueWidget;
+		PathToTemplateRow->GetDefaultWidgets(NameWidget, ValueWidget);
+
+		FName Tag = FName("Step.PathToTemplate");
+		ValueWidget->AddMetadata(FDriverMetaData::Id(Tag));
+
+		PathToTemplateRow->CustomWidget()
+			.NameContent()
+			[
+				NameWidget.ToSharedRef()
+			]
+			.ValueContent()
+			[
+				ValueWidget.ToSharedRef()
+			];	
+	}
+
 	TSharedRef<IPropertyHandle> EnvironmentsHandle = MainDetailLayout->GetProperty("Environments");
 	IDetailPropertyRow* EnvironmentsRow = MainDetailLayout->EditDefaultProperty(EnvironmentsHandle);
 	TSharedPtr<SWidget> OutNameWidgetEnv;
@@ -531,13 +555,13 @@ bool FDeadlineCloudStepParametersArrayBuilder::IsEyeWidgetEnabled(FName Paramete
 	return result;
 }
 
-TSharedRef<FDeadlineCloudStepParameterListBuilder> FDeadlineCloudStepParameterListBuilder::MakeInstance(TSharedRef<IPropertyHandle> InPropertyHandle, EValueType Type)
+TSharedRef<FDeadlineCloudStepParameterListBuilder> FDeadlineCloudStepParameterListBuilder::MakeInstance(TSharedRef<IPropertyHandle> InPropertyHandle, EValueType Type, FString Name)
 {
 	TSharedRef<FDeadlineCloudStepParameterListBuilder> Builder =
 		MakeShared<FDeadlineCloudStepParameterListBuilder>(InPropertyHandle);
 
 	Builder->Type = Type;
-
+	Builder->Name = Name;
 	Builder->OnGenerateArrayElementWidget(
 		FOnGenerateArrayElementWidget::CreateSP(Builder, &FDeadlineCloudStepParameterListBuilder::OnGenerateEntry));
 	return Builder;
@@ -584,7 +608,6 @@ void FDeadlineCloudStepParameterListBuilder::GenerateWrapperStructHeaderRowConte
 
 void FDeadlineCloudStepParameterListBuilder::OnGenerateEntry(TSharedRef<IPropertyHandle> ElementProperty, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder) const
 {
-
 	IDetailPropertyRow& PropertyRow = ChildrenBuilder.AddProperty(ElementProperty);
 
 	// Hide the reset to default button since it provides little value
@@ -596,7 +619,10 @@ void FDeadlineCloudStepParameterListBuilder::OnGenerateEntry(TSharedRef<IPropert
 
 	TSharedPtr<SWidget> NameWidget;
 	TSharedPtr<SWidget> ValueWidget;
-
+	
+	TSharedPtr<SWidget> CustomWidget = FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(ElementProperty, Type, EValueValidationType::StepParameterValue);
+	FName Tag = FName("StepParameter." + Name);
+	CustomWidget->AddMetadata(FDriverMetaData::Id(Tag));
 
 	PropertyRow.GetDefaultWidgets(NameWidget, ValueWidget);
 
@@ -612,10 +638,10 @@ void FDeadlineCloudStepParameterListBuilder::OnGenerateEntry(TSharedRef<IPropert
 		.HAlign(HAlign_Fill)
 		.VAlign(VAlign_Center)
 		[
-			FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(ElementProperty, Type)
+			CustomWidget.ToSharedRef()
 		];
 
-	ValueWidget.ToSharedRef()->SetEnabled(
+	CustomWidget.ToSharedRef()->SetEnabled(
 		TAttribute<bool>::CreateLambda([this]()
 			{
 				if (OnIsEnabled.IsBound())
@@ -630,7 +656,7 @@ void FDeadlineCloudStepParameterListCustomization::CustomizeHeader(TSharedRef<IP
 	TSharedPtr<IPropertyHandle> ArrayHandle = InPropertyHandle->GetChildHandle("Range", false);
 
 	const TSharedPtr<IPropertyHandle> TypeHandle = InPropertyHandle->GetChildHandle("Type", false);
-
+	const TSharedPtr<IPropertyHandle> NameHandle = InPropertyHandle->GetChildHandle("Name", false);
 	if (!TypeHandle.IsValid())
 	{
 		UE_LOG(LogTemp, Error, TEXT("FDeadlineCloudStepParameterListBuilder Type handle is not valid"));
@@ -642,7 +668,10 @@ void FDeadlineCloudStepParameterListCustomization::CustomizeHeader(TSharedRef<IP
 
 	auto Type = (EValueType)TypeValue;
 
-	ArrayBuilder = FDeadlineCloudStepParameterListBuilder::MakeInstance(ArrayHandle.ToSharedRef(), Type);
+	FString NameValue;
+	NameHandle->GetValue(NameValue);
+
+	ArrayBuilder = FDeadlineCloudStepParameterListBuilder::MakeInstance(ArrayHandle.ToSharedRef(), Type, NameValue);
 
 	ArrayBuilder->GenerateWrapperStructHeaderRowContent(InHeaderRow, InPropertyHandle->CreatePropertyNameWidget());
 }
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.cpp
index 937213ca..e9cee8f6 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/MovieRenderPipeline/MoviePipelineDeadlineCloudExecutorJob.cpp
@@ -8,6 +8,7 @@
 #include "Misc/Paths.h"
 #include "Interfaces/IPluginManager.h"
 #include "PropertyEditorModule.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h"
 
 UMoviePipelineDeadlineCloudExecutorJob::UMoviePipelineDeadlineCloudExecutorJob()
 {
@@ -398,6 +399,25 @@ void FMoviePipelineDeadlineCloudExecutorJobCustomization::CustomizeDetails(IDeta
 			DetailBuilder.ForceRefreshDetails();
 		});
 
+	for (auto& Property : OutMrpCategoryProperties)
+	{
+		if (Property->GetProperty()->GetFName() == GET_MEMBER_NAME_CHECKED(UMoviePipelineDeadlineCloudExecutorJob, JobName))
+		{
+			MrpCategory.AddProperty(Property)
+				.CustomWidget()
+				.NameContent()
+				[
+					Property->CreatePropertyNameWidget()
+				]
+				.ValueContent()
+				[
+					FDeadlineCloudDetailsWidgetsHelper::CreatePropertyWidgetByType(
+						Property, EValueType::STRING, EValueValidationType::JobName
+					)
+				];
+		}
+	}
+
 	TSharedPtr<IPropertyHandle> JobPropertyHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMoviePipelineDeadlineCloudExecutorJob, JobPreset));
 	if (!JobPropertyHandle.IsValid()) return;
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Environment.spec.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Environment.spec.cpp
index 53adcf0e..d935d665 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Environment.spec.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Environment.spec.cpp
@@ -19,7 +19,7 @@
 #include "PythonAPILibraries/PythonParametersConsistencyChecker.h"
 
 
-BEGIN_DEFINE_SPEC(FDeadlinePluginEnvironmentSpec, "Deadline",
+BEGIN_DEFINE_SPEC(FDeadlinePluginEnvironmentSpec, "DeadlineCloud",
     EAutomationTestFlags::ProductFilter | EAutomationTestFlags::EditorContext);
 
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Job.spec.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Job.spec.cpp
index 8ad6a144..6e794fd7 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Job.spec.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Job.spec.cpp
@@ -19,7 +19,7 @@
 #include "PythonAPILibraries/PythonParametersConsistencyChecker.h"
 
 
-BEGIN_DEFINE_SPEC(FDeadlinePluginJobSpec, "Deadline",
+BEGIN_DEFINE_SPEC(FDeadlinePluginJobSpec, "DeadlineCloud",
     EAutomationTestFlags::ProductFilter | EAutomationTestFlags::EditorContext);
 
 UDeadlineCloudJob* CreatedJobDataAsset;
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Step.spec.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Step.spec.cpp
index 612ecc11..01b9c4ce 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Step.spec.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_Step.spec.cpp
@@ -19,7 +19,7 @@
 #include "PythonAPILibraries/PythonParametersConsistencyChecker.h"
 
 
-BEGIN_DEFINE_SPEC(FDeadlinePluginStepSpec, "Deadline",
+BEGIN_DEFINE_SPEC(FDeadlinePluginStepSpec, "DeadlineCloud",
     EAutomationTestFlags::ProductFilter | EAutomationTestFlags::EditorContext);
 
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_UI.spec.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_UI.spec.cpp
new file mode 100644
index 00000000..cf8acaf0
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/DeadlinePluginTest_UI.spec.cpp
@@ -0,0 +1,737 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#pragma once
+#include "Misc/AutomationTest.h"
+#include "CoreMinimal.h"
+#include "Engine/Engine.h"
+#include "UObject/UObjectGlobals.h"
+#include "AssetToolsModule.h"
+#include "Engine/AssetManager.h"
+#include "AssetRegistry/AssetRegistryModule.h"
+#include "AssetRegistry/IAssetRegistry.h"
+#include "Misc/Paths.h"
+#include "Interfaces/IPluginManager.h"
+#include "ObjectTools.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudJob.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudStep.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudEnvironment.h"
+#include "PythonAPILibraries/PythonYamlLibrary.h"
+#include "PythonAPILibraries/DeadlineCloudJobBundleLibrary.h"
+#include "PythonAPILibraries/PythonParametersConsistencyChecker.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
+
+#include "Tests/AutomationCommon.h"
+#include "Subsystems/AssetEditorSubsystem.h"
+#include "AutomationDriverTypeDefs.h"
+#include "IAutomationDriver.h"
+#include "IAutomationDriverModule.h"
+#include "IDriverElement.h"
+#include "IDriverSequence.h"
+#include "LocateBy.h"
+
+#include "PropertyEditorModule.h"
+#include "IDetailsView.h"
+
+#define TEST_TRUE(expression) \
+	EPIC_TEST_BOOLEAN_(TEXT(#expression), expression, true)
+
+#define TEST_FALSE(expression) \
+	EPIC_TEST_BOOLEAN_(TEXT(#expression), expression, false)
+
+#define TEST_EQUAL(expression, expected) \
+	EPIC_TEST_BOOLEAN_(TEXT(#expression), expression, expected)
+
+#define EPIC_TEST_BOOLEAN_(text, expression, expected) \
+	TestEqual(text, expression, expected);
+
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIsValidLength_RangeOK, "DeadlineCloud.Validation.IsValidLength.RangeOK", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FIsValidLength_RangeOK::RunTest(const FString& Parameters)
+{
+    FText Error;
+    FString Input = FString::ChrN(10, 'A');
+
+    bool Result = FDeadlineCloudInputValidationHelper::IsValidLength(Input, 5, 15, Error);
+    TestTrue("Length 10 in range 5-15", Result);
+    TestTrue("Error should be empty", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIsValidLength_TooShort, "DeadlineCloud.Validation.IsValidLength.TooShort", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FIsValidLength_TooShort::RunTest(const FString& Parameters)
+{
+    FText Error;
+    FString Input = "Hi";
+
+    bool Result = FDeadlineCloudInputValidationHelper::IsValidLength(Input, 3, 10, Error);
+    TestFalse("Length 2 is too short", Result);
+    TestFalse("Error should be set", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FContainsNoControlChars_Valid, "DeadlineCloud.Validation.ControlChars.Valid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FContainsNoControlChars_Valid::RunTest(const FString& Parameters)
+{
+    FString Input = TEXT("Hello\nWorld\t!");
+    TSet<TCHAR> Exclude = { '\n', '\t' };
+    FText Error;
+
+    bool Result = FDeadlineCloudInputValidationHelper::ContainsNoControlCharacters(Input, Error, Exclude);
+    TestTrue("Allowed control characters", Result);
+    TestTrue("Error should be empty", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FContainsNoControlChars_Invalid, "DeadlineCloud.Validation.ControlChars.Invalid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FContainsNoControlChars_Invalid::RunTest(const FString& Parameters)
+{
+    FString Input;
+    Input += TCHAR(1); // Control character
+    FText Error;
+
+    bool Result = FDeadlineCloudInputValidationHelper::ContainsNoControlCharacters(Input, Error, {});
+    TestFalse("Disallowed control character", Result);
+    TestFalse("Error should be set", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FValidIdentifier, "DeadlineCloud.Validation.Identifier.Valid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FValidIdentifier::RunTest(const FString& Parameters)
+{
+    FText Error;
+    FString Input = TEXT("_Valid123");
+
+    bool Result = FDeadlineCloudInputValidationHelper::IsValidIdentifier(Input, Error);
+    TestTrue("Valid identifier", Result);
+    TestTrue("Error should be empty", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInvalidIdentifier_StartsWithNumber, "DeadlineCloud.Validation.Identifier.InvalidStart", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FInvalidIdentifier_StartsWithNumber::RunTest(const FString& Parameters)
+{
+    FText Error;
+    FString Input = TEXT("1Invalid");
+
+    bool Result = FDeadlineCloudInputValidationHelper::IsValidIdentifier(Input, Error);
+    TestFalse("Starts with number", Result);
+    TestFalse("Error should be set", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInvalidIdentifier_IllegalChar, "DeadlineCloud.Validation.Identifier.IllegalChar", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FInvalidIdentifier_IllegalChar::RunTest(const FString& Parameters)
+{
+    FText Error;
+    FString Input = TEXT("Valid$Name");
+
+    bool Result = FDeadlineCloudInputValidationHelper::IsValidIdentifier(Input, Error);
+    TestFalse("Illegal character in identifier", Result);
+    TestFalse("Error should be set", Error.IsEmpty());
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FJobParameterValue_Valid, "DeadlineCloud.Validation.String.JobParam.Valid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FJobParameterValue_Valid::RunTest(const FString& Parameters)
+{
+    const auto Validator = FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType::JobParameterValue);
+
+    FText Error;
+    bool Result = Validator.Execute(FText::FromString("SomeValue"), Error);
+
+    TestTrue("Valid JobParameterValue", Result);
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FPathValidator_ValidStepParameter, "DeadlineCloud.Validation.Path.StepParam.Valid", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+bool FPathValidator_ValidStepParameter::RunTest(const FString& Parameters)
+{
+    const auto Validator = FDeadlineCloudInputValidationHelper::GetPathValidationFunction(EValueValidationType::StepParameterValue);
+
+    FText Error;
+    bool Result = Validator.Execute(FText::FromString("C:/Temp/File.txt"), Error);
+
+    TestTrue("Valid path", Result);
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInvalidLengthTest, "DeadlineCloud.Validation.InvalidLength", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+
+bool FInvalidLengthTest::RunTest(const FString& Parameters)
+{
+    FText Error;
+    // Too short (Min=1)
+    TestFalse("Empty string should fail for Min=1", FDeadlineCloudInputValidationHelper::IsValidLength(TEXT(""), 1, 10, Error));
+    
+    // Too long
+    TestFalse("Too long string should fail for Max=10", FDeadlineCloudInputValidationHelper::IsValidLength(TEXT("12345678901"), 1, 10, Error));
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInvalidIdentifierTest, "DeadlineCloud.Validation.InvalidIdentifier", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+
+bool FInvalidIdentifierTest::RunTest(const FString& Parameters)
+{
+    FText Error;
+    // Starts with a digit
+    TestFalse("Identifier starting with digit should be invalid", FDeadlineCloudInputValidationHelper::IsValidIdentifier(TEXT("1abc"), Error));
+
+    // Contains special characters
+    TestFalse("Identifier with special chars should be invalid", FDeadlineCloudInputValidationHelper::IsValidIdentifier(TEXT("abc@def"), Error));
+
+    // Empty
+    TestFalse("Empty identifier should be invalid", FDeadlineCloudInputValidationHelper::IsValidIdentifier(TEXT(""), Error));
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FControlCharacterTest, "DeadlineCloud.Validation.ControlCharacters", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+
+bool FControlCharacterTest::RunTest(const FString& Parameters)
+{
+    FText Error;
+
+    TSet<TCHAR> Allowed = { '\n', '\t' };
+
+    // String with disallowed control char (e.g. ASCII 1)
+    FString BadStr;
+    BadStr.AppendChar(1); // SOH character
+
+    TestFalse("String with disallowed control characters should fail", FDeadlineCloudInputValidationHelper::ContainsNoControlCharacters(BadStr, Error, Allowed));
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FValidationFunction_JobName_Invalid, "DeadlineCloud.Validation.JobName.InvalidCases", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+
+bool FValidationFunction_JobName_Invalid::RunTest(const FString& Parameters)
+{
+    FText Error;
+    auto Validator = FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType::JobName);
+
+    // Too long
+    FString TooLong = FString::ChrN(65, 'a');
+    TestFalse("JobName too long", Validator.Execute(FText::FromString(TooLong), Error));
+
+    // Invalid chars
+    TestFalse("JobName with ! character should fail", Validator.Execute(FText::FromString("My!Job"), Error));
+
+    return true;
+}
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FValidationFunction_JobDescription_Invalid, "DeadlineCloud.Validation.JobDescription.ControlChar", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
+
+bool FValidationFunction_JobDescription_Invalid::RunTest(const FString& Parameters)
+{
+    FText Error;
+    auto Validator = FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType::JobDescription);
+
+    FString BadDesc = TEXT("Hello");
+    BadDesc.AppendChar(3); // ETX control character
+
+    TestFalse("JobDescription with disallowed control character", Validator.Execute(FText::FromString(BadDesc), Error));
+
+    return true;
+}
+
+static FString ConvertLocalPathToFull(const FString& Path)
+{
+	FString PluginContentDir = IPluginManager::Get().FindPlugin(TEXT("UnrealDeadlineCloudService"))->GetBaseDir();
+	PluginContentDir = FPaths::ConvertRelativePathToFull(PluginContentDir);
+	FString FullPath = FPaths::Combine(PluginContentDir, Path);
+	FPaths::NormalizeDirectoryName(FullPath);
+	return FullPath;
+}
+
+static void ExpandAllProperties(const FString DetailsPath, FAutomationDriverPtr Driver)
+{
+	FString MainCategoryExpanderArrowPath = DetailsPath + "//<SDetailCategoryTableRow>//<SDetailExpanderArrow>";
+	FDriverElementCollectionRef ParametersCategory = Driver->FindElements(By::Path(MainCategoryExpanderArrowPath));
+	ParametersCategory->GetElements()[0]->Click(EMouseButtons::Type::Right);
+	Driver->Wait(FTimespan::FromSeconds(1));
+
+	FString PopupElementsPath = "<SWindow>//<SPopup>//<SMultiBoxWidget>//<SBorder>//<SVerticalBox>//<SScrollBox>//<SHorizontalBox>//<SOverlay>//<SScrollPanel>//<SVerticalBox>//<SHorizontalBox>//<SMenuEntryButton>";
+
+	FDriverElementCollectionRef PopupElements = Driver->FindElements(By::Path(PopupElementsPath));
+	if (!PopupElements->GetElements().IsEmpty())
+	{
+		PopupElements->GetElements()[2]->Focus();
+		PopupElements->GetElements()[2]->Click(EMouseButtons::Type::Left);
+	}
+}
+
+static void ScrollToElement(FAutomationDriverPtr Driver, FDriverElementRef List, FDriverElementRef ScrollBar, FDriverElementRef TargetElement, uint32 AttemptsLimit)
+{
+	if (TargetElement->Exists() && TargetElement->IsVisible())
+	{
+		return;
+	}
+
+	if (List->Exists() && ScrollBar->Exists())
+	{
+		uint32 CurrentAttempts = 0;
+		while ((!TargetElement->Exists() || !TargetElement->IsVisible()) && (!ScrollBar->IsScrolledToEnd() && CurrentAttempts < AttemptsLimit))
+		{
+			List->ScrollBy(-1);
+			CurrentAttempts++;
+		}
+	}
+}
+
+template<typename AssetType>
+AssetType* CreateAndOpenAsset(
+    const FString& RelativeTemplatePath,
+    FString& OutFullTemplatePath)
+{
+    OutFullTemplatePath = ConvertLocalPathToFull(RelativeTemplatePath);
+    AssetType* Asset = NewObject<AssetType>();
+    Asset->PathToTemplate.FilePath = OutFullTemplatePath;
+
+    if constexpr (std::is_same_v<AssetType, UDeadlineCloudJob>)
+    {
+        Asset->OpenJobFile(OutFullTemplatePath);
+    }
+    else if constexpr (std::is_same_v<AssetType, UDeadlineCloudStep>)
+    {
+        Asset->OpenStepFile(OutFullTemplatePath);
+    }
+    else if constexpr (std::is_same_v<AssetType, UDeadlineCloudEnvironment>)
+    {
+        Asset->OpenEnvFile(OutFullTemplatePath);
+    }
+
+    auto* Editor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
+    Editor->CloseAllAssetEditors();
+    Editor->OpenEditorForAsset(Asset);
+    return Asset;
+}
+
+static void InputText(FDriverElementRef Widget, const FString& Text, bool bRemoveTextBeforeInput)
+{
+	if (bRemoveTextBeforeInput)
+	{
+		Widget->TypeChord(EKeys::LeftControl, EKeys::A);
+		Widget->Type(EKeys::Delete);
+	}
+	if (!Text.IsEmpty())
+	{
+		Widget->Type(Text);
+	}
+	Widget->Type(EKeys::Enter);
+}
+
+
+
+BEGIN_DEFINE_SPEC(FDeadlinePluginUISpec, "DeadlineCloud",
+    EAutomationTestFlags::ProductFilter | EAutomationTestFlags::EditorContext | EAutomationTestFlags::NonNullRHI);
+
+FAutomationDriverPtr Driver;
+UDeadlineCloudStep* CreatedStepDataAsset;
+UDeadlineCloudEnvironment* CreatedEnvironmentDataAsset;
+UDeadlineCloudJob* CreatedJobDataAsset;
+FParametersConsistencyCheckResult result;
+
+FString PathToStepTemplate;
+FString StepTemplate = "/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_step_UI.yml";
+FString PathToEnvironmentTemplate;
+FString EnvTemplate = "/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/launch_ue_environment_UI.yml";
+FString PathToJobTemplate;
+FString JobTemplate = "/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_job_UI.yml";
+
+const FString DetailsPath = "<SStandaloneAssetEditorToolkitHost>//<SDetailsView>";
+const FString ListPath = DetailsPath + "//<SListPanel>";
+const FString ScrollBarPath = DetailsPath + "//<SScrollBar>";
+
+FDriverElementPtr Details;
+FDriverElementPtr List;
+FDriverElementPtr ScrollBar;
+
+inline bool Init(UObject* Asset)
+{
+    if (!IsValid(Asset))
+    {
+        TestTrue(TEXT("Asset should exist"), false);
+        return false;
+    }
+    // Locate Details View
+    Details = Driver->FindElement(By::Path(DetailsPath));
+    Driver->Wait(Until::ElementExists(Details.ToSharedRef(), FWaitTimeout::InSeconds(2.f)));
+    if (!Details->Exists())
+    {
+        TestTrue(TEXT("Details view should exist"), false);
+        return false;
+    }
+    Details->Focus();
+
+    // Locate List and ScrollBar
+    List = Driver->FindElement(By::Path(ListPath));
+    if (!List->Exists())
+    {
+        TestTrue(TEXT("List widget should exist"), false);
+        return false;
+    }
+    ScrollBar = Driver->FindElement(By::Path(ScrollBarPath));
+    return true;
+}
+
+END_DEFINE_SPEC(FDeadlinePluginUISpec);
+
+void FDeadlinePluginUISpec::Define()
+{
+	BeforeEach([this]() {
+		if (IAutomationDriverModule::Get().IsEnabled())
+		{
+			IAutomationDriverModule::Get().Disable();
+		}
+
+		IAutomationDriverModule::Get().Enable();
+
+		Driver = IAutomationDriverModule::Get().CreateDriver();
+		});
+
+    Describe("DeadlineCloudJobUI", [this]()
+    {
+		BeforeEach([this]() {
+			CreatedJobDataAsset = CreateAndOpenAsset<UDeadlineCloudJob>(JobTemplate, PathToJobTemplate);
+			});
+
+		It("JobUI", EAsyncExecution::ThreadPool, [this]() {
+			if (!Init(CreatedJobDataAsset))
+			{
+				return;
+			}
+
+			ExpandAllProperties(DetailsPath, Driver);
+
+			FString JobNamePath = DetailsPath + "//#JobPreset.Name//<SEditableTextBox>";
+			FString DescriptionPath = DetailsPath + "//#JobPreset.Description//<SEditableTextBox>";
+
+			FString StringParametersPath = DetailsPath + "//#JobParameter.StringParameter//<SEditableTextBox>";
+			FString PathParametersPath = DetailsPath + "//#JobParameter.PathParameter//<SEditableTextBox>";
+			FString FloatParametersPath = DetailsPath + "//#JobParameter.FloatParameter//<SEditableText>";
+			FString IntParametersPath = DetailsPath + "//#JobParameter.IntParameter//<SEditableText>";
+
+			FDriverElementRef JobNameWidget = Driver->FindElement(By::Path(JobNamePath));
+			FDriverElementRef DescriptionWidget = Driver->FindElement(By::Path(DescriptionPath));
+
+			FDriverElementRef StringParametersWidget = Driver->FindElement(By::Path(StringParametersPath));
+			FDriverElementRef PathParametersPathWidget = Driver->FindElement(By::Path(PathParametersPath));
+			FDriverElementRef FloatParametersWidget = Driver->FindElement(By::Path(FloatParametersPath));
+			FDriverElementRef IntParametersWidget = Driver->FindElement(By::Path(IntParametersPath));
+
+			//JobName
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), JobNameWidget, 50);
+			bool bJobNameWidgetExists = JobNameWidget->Exists();
+			TestTrue("JobName widget should exist", bJobNameWidgetExists);
+			if (bJobNameWidgetExists)
+			{
+				FString OldValue = CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Name;
+				InputText(JobNameWidget, "123 Invalid", true);
+				TEST_EQUAL(CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Name, OldValue);
+
+				InputText(JobNameWidget, "", true);
+				TEST_EQUAL(CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Name, OldValue);
+
+				FString ValidJobName = "ValidJob123";
+				InputText(JobNameWidget, ValidJobName, true);
+				TEST_EQUAL(CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Name, ValidJobName);
+			}
+
+			//Description
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), DescriptionWidget, 50);
+			bool bDescriptionWidgetExists = DescriptionWidget->Exists();
+			TestTrue("Description widget should exist", bDescriptionWidgetExists);
+			if (bDescriptionWidgetExists)
+			{
+				FString LongString;
+				for (int i = 0; i < 2045; ++i) LongString += TEXT("A");
+
+				CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Description = LongString;
+				InputText(DescriptionWidget, "LongString", false);
+				TEST_EQUAL(CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Description, LongString);
+
+				FString ValidDescription = TEXT("This is a job description.");
+				InputText(DescriptionWidget, ValidDescription, true);
+				TEST_EQUAL(CreatedJobDataAsset->JobPresetStruct.JobSharedSettings.Description, ValidDescription);	
+			}
+
+			//PathParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), StringParametersWidget, 50);
+			bool bPathParametersWidgetExists = StringParametersWidget->Exists();
+			TestTrue("StringParameters widget should exist", bPathParametersWidgetExists);
+			if (bPathParametersWidgetExists)
+			{
+				FString PathParameterOldValue = CreatedJobDataAsset->ParameterDefinition.Parameters[0].Value;
+				FString PathParametersText = "ThisInputIsWayTooLongForValidation";
+				//Click on the widget to make it editable and remove text selection
+				PathParametersPathWidget->Click(EMouseButtons::Type::Left);
+				PathParametersPathWidget->Type(EKeys::Left);
+				PathParametersPathWidget->Type(PathParametersText);
+				PathParametersPathWidget->Type(EKeys::Enter);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[0].Value, PathParameterOldValue);
+
+				InputText(PathParametersPathWidget, "", true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[0].Value, "");
+
+				FString PathParametersTextValid = "ValidString";
+				InputText(PathParametersPathWidget, PathParametersTextValid, true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[0].Value, PathParametersTextValid);							
+			}
+
+			//StringParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), StringParametersWidget, 50);
+			bool bStringParametersWidgetExists = StringParametersWidget->Exists();
+			TestTrue("StringParameters widget should exist", bStringParametersWidgetExists);
+			if (bStringParametersWidgetExists)
+			{
+				FString StringParameterOldValue = CreatedJobDataAsset->ParameterDefinition.Parameters[1].Value;
+				InputText(StringParametersWidget, "ThisInputIsWayTooLongForValidation", false);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[1].Value, StringParameterOldValue);
+
+				InputText(StringParametersWidget, "", true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[1].Value, "");
+
+				FString StringParametersTextValid = "ValidString";
+				InputText(StringParametersWidget, StringParametersTextValid, true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[1].Value, StringParametersTextValid);							
+			}
+
+			//FloatParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), FloatParametersWidget, 50);
+			bool bFloatParametersWidgetExists = FloatParametersWidget->Exists();
+			TestTrue("FloatParameters widget should exist", bFloatParametersWidgetExists);
+			if (bFloatParametersWidgetExists)
+			{
+				FString FloatParametersOldValue = CreatedJobDataAsset->ParameterDefinition.Parameters[2].Value;
+
+				InputText(FloatParametersWidget, "InvalidValue", false);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[2].Value, FloatParametersOldValue);
+
+				InputText(FloatParametersWidget, "", true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[2].Value, FloatParametersOldValue);
+
+				FString FloatParametersTextValid = "123.456";
+				InputText(FloatParametersWidget, FloatParametersTextValid, true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[2].Value, FloatParametersTextValid);							
+			}
+
+			//IntParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), IntParametersWidget, 50);
+			bool bIntParametersWidgetExists = IntParametersWidget->Exists();
+			TestTrue("IntParameters widget should exist", bIntParametersWidgetExists);
+			if (bIntParametersWidgetExists)
+			{
+				FString IntParametersOldValue = CreatedJobDataAsset->ParameterDefinition.Parameters[3].Value;
+
+				InputText(IntParametersWidget, "InvalidValue", true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[3].Value, IntParametersOldValue);
+
+				InputText(IntParametersWidget, "", true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[3].Value, IntParametersOldValue);
+
+				FString IntParametersTextValid = "123";
+				InputText(IntParametersWidget, IntParametersTextValid, true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[3].Value, IntParametersTextValid);
+
+				FString IntParametersTextInvalid = "123.456";
+				InputText(IntParametersWidget, IntParametersTextInvalid, true);
+				TEST_EQUAL(CreatedJobDataAsset->ParameterDefinition.Parameters[3].Value, IntParametersTextValid);							
+			}		
+			});
+
+        AfterEach([this]()
+            {
+                CreatedJobDataAsset = nullptr;
+            });
+    });
+
+    Describe("DeadlineCloudStepUI", [this]()
+    {
+		BeforeEach([this]() {
+			CreatedStepDataAsset = CreateAndOpenAsset<UDeadlineCloudStep>(StepTemplate, PathToStepTemplate);
+			});
+
+		It("StepUI", EAsyncExecution::ThreadPool, [this]() {
+			if (!Init(CreatedStepDataAsset))
+			{
+				return;
+			}
+
+			ExpandAllProperties(DetailsPath, Driver);
+
+			FString StringParametersPath = DetailsPath + "//#StepParameter.StringParameters//<SEditableTextBox>";
+			FString PathParametersPath = DetailsPath + "//#StepParameter.PathParameters//<SEditableTextBox>";
+			FString FloatParametersPath = DetailsPath + "//#StepParameter.FloatParameters//<SEditableText>";
+			FString IntParametersPath = DetailsPath + "//#StepParameter.IntParameters//<SEditableText>";
+
+			FDriverElementRef StringParametersWidget = Driver->FindElement(By::Path(StringParametersPath));
+			FDriverElementRef PathParametersPathWidget = Driver->FindElement(By::Path(PathParametersPath));
+			FDriverElementRef FloatParametersWidget = Driver->FindElement(By::Path(FloatParametersPath));
+			FDriverElementRef IntParametersWidget = Driver->FindElement(By::Path(IntParametersPath));
+
+			//StringParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), StringParametersWidget, 50);
+			bool bStringParametersWidgetExists = StringParametersWidget->Exists();
+			TestTrue("StringParameters widget should exist", bStringParametersWidgetExists);
+			if (bStringParametersWidgetExists)
+			{
+				FStepTaskParameterDefinition StringParameter = CreatedStepDataAsset->TaskParameterDefinitions.Parameters[0];
+				TEST_TRUE(StringParameter.Type == EValueType::STRING)
+				FString StringParameterOldValue = StringParameter.Range[0];
+
+				InputText(StringParametersWidget, "ThisInputIsWayTooLongForValidation", false);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[0].Range[0], StringParameterOldValue);
+
+				InputText(StringParametersWidget, "", true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[0].Range[0], StringParameterOldValue);
+
+				FString StringParametersTextValid = "ValidString";
+				InputText(StringParametersWidget, StringParametersTextValid, true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[0].Range[0], StringParametersTextValid);
+			}
+
+			//PathParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), PathParametersPathWidget, 50);
+			bool bPathParametersWidgetExists = PathParametersPathWidget->Exists();
+			TestTrue("PathParameters widget should exist", bPathParametersWidgetExists);
+			if (bPathParametersWidgetExists)
+			{
+				FStepTaskParameterDefinition PathParameter = CreatedStepDataAsset->TaskParameterDefinitions.Parameters[1];
+				TEST_TRUE(PathParameter.Type == EValueType::PATH)
+
+				FString PathParameterOldValue = PathParameter.Range[0];
+				FString PathParametersText = "ThisInputIsWayTooLongForValidation";
+				//Click on the widget to make it editable and remove text selection
+				PathParametersPathWidget->Click(EMouseButtons::Type::Left);
+				PathParametersPathWidget->Type(EKeys::Left);
+				PathParametersPathWidget->Type(PathParametersText);
+				PathParametersPathWidget->Type(EKeys::Enter);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[1].Range[0], PathParameterOldValue);
+
+				InputText(PathParametersPathWidget, "", true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[1].Range[0], PathParameterOldValue);
+
+				FString PathParametersTextValid = "ValidString";
+				InputText(PathParametersPathWidget, PathParametersTextValid, true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[1].Range[0], PathParametersTextValid);
+			}
+
+			//FloatParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), FloatParametersWidget, 50);
+			bool bFloatParametersWidgetExists = FloatParametersWidget->Exists();
+			TestTrue("FloatParameters widget should exist", bFloatParametersWidgetExists);
+			if (bFloatParametersWidgetExists)
+			{
+				FStepTaskParameterDefinition FloatParameter = CreatedStepDataAsset->TaskParameterDefinitions.Parameters[2];
+				TEST_TRUE(FloatParameter.Type == EValueType::FLOAT)
+
+				FString FloatParameterOldValue = FloatParameter.Range[0];
+				InputText(FloatParametersWidget, "InvalidValue", false);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[2].Range[0], FloatParameterOldValue);
+
+				InputText(FloatParametersWidget, "", true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[2].Range[0], FloatParameterOldValue);
+
+				FString FloatParametersTextValid = "123.456";
+				InputText(FloatParametersWidget, FloatParametersTextValid, true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[2].Range[0], FloatParametersTextValid);
+			}
+
+			//IntParameter
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), IntParametersWidget, 50);
+			bool bIntParametersWidgetExists = IntParametersWidget->Exists();
+			TestTrue("IntParameters widget should exist", bIntParametersWidgetExists);
+			if (bIntParametersWidgetExists)
+			{
+				FStepTaskParameterDefinition IntParameter = CreatedStepDataAsset->TaskParameterDefinitions.Parameters[3];
+				TEST_TRUE(IntParameter.Type == EValueType::INT)
+
+				FString IntParameterOldValue = IntParameter.Range[0];
+				InputText(IntParametersWidget, "InvalidValue", false);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[3].Range[0], IntParameterOldValue);
+
+				InputText(IntParametersWidget, "", true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[3].Range[0], IntParameterOldValue);
+
+				FString IntParametersTextValid = "123";
+				InputText(IntParametersWidget, IntParametersTextValid, true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[3].Range[0], IntParametersTextValid);
+
+				FString IntParametersTextInvalid = "123.456";
+				InputText(IntParametersWidget, IntParametersTextInvalid, true);
+				TEST_EQUAL(CreatedStepDataAsset->TaskParameterDefinitions.Parameters[3].Range[0], IntParametersTextValid);
+			}
+		});
+
+        AfterEach([this]()
+            {
+                CreatedStepDataAsset = nullptr;
+            });
+    });
+
+    Describe("DeadlineCloudEnvironmentUI", [this]()
+    {
+		BeforeEach([this]() {
+			CreatedEnvironmentDataAsset = CreateAndOpenAsset<UDeadlineCloudEnvironment>(EnvTemplate, PathToEnvironmentTemplate);
+			});
+
+		It("EnvironmentUI", EAsyncExecution::ThreadPool, [this]() {
+			if (!Init(CreatedEnvironmentDataAsset))
+			{
+				return;
+			}
+
+			ExpandAllProperties(DetailsPath, Driver);
+
+			FString Variable1Path = DetailsPath + "//#EnvironmentParameter.Variable1//<SEditableTextBox>";
+			FString Variable2Path = DetailsPath + "//#EnvironmentParameter.Variable2//<SEditableTextBox>";
+			FString Variable3Path = DetailsPath + "//#EnvironmentParameter.Variable3//<SEditableTextBox>";
+
+			FDriverElementRef Variable1Widget = Driver->FindElement(By::Path(Variable1Path));
+			FDriverElementRef Variable2Widget = Driver->FindElement(By::Path(Variable2Path));
+			FDriverElementRef Variable3Widget = Driver->FindElement(By::Path(Variable3Path));
+
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), Variable1Widget, 50);
+			bool bVariable1WidgetExists = Variable1Widget->Exists();
+			TestTrue("Variable1 widget should exist", bVariable1WidgetExists);
+			if (bVariable1WidgetExists)
+			{
+				InputText(Variable1Widget, "", true);
+				TEST_EQUAL(CreatedEnvironmentDataAsset->Variables.Variables["Variable1"], "");
+
+				FString Variable1TextValid = "ValidString";
+				InputText(Variable1Widget, Variable1TextValid, true);
+				TEST_EQUAL(CreatedEnvironmentDataAsset->Variables.Variables["Variable1"], Variable1TextValid);
+			}
+
+			ScrollToElement(Driver, List.ToSharedRef(), ScrollBar.ToSharedRef(), Variable2Widget, 50);
+			bool bVariable2WidgetExists = Variable2Widget->Exists();
+			TestTrue("Variable2 widget should exist", bVariable2WidgetExists);
+			if (bVariable2WidgetExists)
+			{
+				FString Variable2OldValue = CreatedEnvironmentDataAsset->Variables.Variables["Variable2"];
+				InputText(Variable2Widget, "ThisInputIsWayTooLongForValidation", false);
+				TEST_EQUAL(CreatedEnvironmentDataAsset->Variables.Variables["Variable2"], Variable2OldValue);
+			}
+		});
+
+        AfterEach([this]()
+			{
+				CreatedEnvironmentDataAsset = nullptr;
+		});
+    });
+
+	AfterEach([this]() {
+		Driver.Reset();
+		IAutomationDriverModule::Get().Disable();
+		});
+}
+
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/Integration/DeadlinePluginTest_CreateJobTest.cpp b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/Integration/DeadlinePluginTest_CreateJobTest.cpp
index eee98760..2ea91ee4 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/Integration/DeadlinePluginTest_CreateJobTest.cpp
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/Integration/DeadlinePluginTest_CreateJobTest.cpp
@@ -464,7 +464,7 @@ ULevelSequence* FindFirstLevelSequence()
     return ShortestPathSequence;
 }
 
-IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieQueueCreateJobTest, "Deadline.Integration.CreateJob",
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieQueueCreateJobTest, "DeadlineCloud.Integration.CreateJob",
     EAutomationTestFlags::EditorContext |
     EAutomationTestFlags::ProductFilter)
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/launch_ue_environment_UI.yml b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/launch_ue_environment_UI.yml
new file mode 100644
index 00000000..4a822806
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/launch_ue_environment_UI.yml
@@ -0,0 +1,36 @@
+name: LaunchUnrealEditor
+variables:
+  Variable1: 'True'
+  Variable2: 'ThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidation'
+  Variable3: 'Test' 
+script:
+  embeddedFiles:
+    - name: initData
+      filename: init-data.yaml
+      type: TEXT
+      data: |
+        executable: {{Param.Executable}}
+        project_path: {{Param.ProjectFilePath}}
+        extra_cmd_args: {{Param.ExtraCmdArgs}}
+        extra_cmd_args_file: {{Param.ExtraCmdArgsFile}}
+  actions:
+    onEnter:
+      command: unreal-engine-openjd
+      args:
+      - daemon
+      - start
+      - --connection-file
+      - '{{Session.WorkingDirectory}}/connection.json'
+      - --init-data
+      - file://{{Env.File.initData}}
+      cancelation:
+        mode: NOTIFY_THEN_TERMINATE
+    onExit:
+      command: unreal-engine-openjd
+      args:
+      - daemon
+      - stop
+      - --connection-file
+      - '{{Session.WorkingDirectory}}/connection.json'
+      cancelation:
+        mode: NOTIFY_THEN_TERMINATE
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_job_UI.yml b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_job_UI.yml
new file mode 100644
index 00000000..b2e8b04c
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_job_UI.yml
@@ -0,0 +1,17 @@
+specificationVersion: jobtemplate-2023-09
+name: RenderJob
+parameterDefinitions:
+- name: PathParameter
+  type: PATH
+  default: 'ThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidation'
+  objectType: FILE
+  dataFlow: IN
+- name: StringParameter
+  type: STRING
+  default: 'ThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidation'
+- name: FloatParameter
+  type: FLOAT
+  default: '3.5'
+- name: IntParameter
+  type: INT
+  default: '5'
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_step_UI.yml b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_step_UI.yml
new file mode 100644
index 00000000..47fecc43
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/openjd_templates/render_step_UI.yml
@@ -0,0 +1,45 @@
+name: Render
+parameterSpace:
+  taskParameterDefinitions:
+  - name: StringParameters
+    type: STRING
+    range: [
+    ThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidation
+    ]
+  - name: PathParameters
+    type: PATH
+    range: [
+    ThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidationThisInputIsWayTooLongForValidation   
+    ]
+  - name: FloatParameters
+    type: FLOAT
+    range: [
+    1.5
+    ]
+  - name: IntParameters
+    type: INT
+    range: [
+    3
+    ]
+script:
+  embeddedFiles:
+  - name: runData
+    filename: run-data.yaml
+    type: TEXT
+    data: |
+      handler: {{Task.Param.Handler}}
+      queue_manifest_path: {{Task.Param.QueueManifestPath}}
+      chunk_size: {{Task.Param.ChunkSize}}
+      chunk_id: {{Task.Param.ChunkId}}
+  actions:
+    onRun:
+      command: unreal-engine-openjd
+      args:
+      - daemon
+      - run
+      - --connection-file
+      - '{{Session.WorkingDirectory}}/connection.json'
+      - --run-data
+      - file://{{Task.File.runData}}
+      cancelation:
+        mode: NOTIFY_THEN_TERMINATE
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h
index ee220a41..21e39e5c 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h
@@ -5,13 +5,16 @@
 #include "PythonAPILibraries/PythonYamlLibrary.h"
 #include "DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h"
 
+DECLARE_DELEGATE_RetVal_OneParam(FText, FIsValidInputSignature, const FText);
+
 class DeadlineCloudJobPresetDetailsCustomization;
 
 class FDeadlineCloudDetailsWidgetsHelper
 {
 public:
 
-	static TSharedRef<SWidget> CreatePropertyWidgetByType(TSharedPtr<IPropertyHandle> ParameterHandle, EValueType Type);
+	static TSharedRef<SWidget> CreatePropertyWidgetByType(TSharedPtr<IPropertyHandle> ParameterHandle, EValueType Type, EValueValidationType ValidationType = EValueValidationType::Default);
+	static TSharedPtr<SWidget> TryCreatePropertyWidgetFromMetadata(TSharedPtr<IPropertyHandle> ParameterHandle);
 	static TSharedRef<SWidget> CreateNameWidget(FString Parameter);
 
 	static TSharedRef<SWidget> CreateConsistencyWidget(FString ResultString);
@@ -86,10 +89,8 @@ class FDeadlineCloudDetailsWidgetsHelper
 	public:
 
 
-		void Construct(const FArguments& InArgs, const FName& InPropertyPath_, const bool bIsChecked_)
-	
+		void Construct(const FArguments& InArgs, const FName& InPropertyPath_, const bool bIsChecked_)	
 		{
-
 			InPropertyPath = InPropertyPath_;
 			bIsChecked = bIsChecked_;
 			
@@ -158,8 +159,8 @@ class FDeadlineCloudDetailsWidgetsHelper
 	
 private:
 
-	static TSharedRef<SWidget> CreatePathWidget(TSharedPtr<IPropertyHandle> ParameterHandle);
+	static TSharedRef<SWidget> CreatePathWidget(TSharedPtr<IPropertyHandle> ParameterHandle, FOnVerifyTextChanged Validation);
 	static TSharedRef<SWidget> CreateIntWidget(TSharedPtr<IPropertyHandle> ParameterHandle);
 	static TSharedRef<SWidget> CreateFloatWidget(TSharedPtr<IPropertyHandle> ParameterHandle);
-	static TSharedRef<SWidget> CreateStringWidget(TSharedPtr<IPropertyHandle> ParameterHandle);
+	static TSharedRef<SWidget> CreateStringWidget(TSharedPtr<IPropertyHandle> ParameterHandle, FOnVerifyTextChanged Validation);
 };
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudEnvironment.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudEnvironment.h
index 6ed73cdc..a6b68582 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudEnvironment.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudEnvironment.h
@@ -49,7 +49,7 @@ class UNREALDEADLINECLOUDSERVICE_API UDeadlineCloudEnvironment : public UDataAss
 	void OpenEnvFile(const FString& Path);
 
 	UFUNCTION()
-	FParametersConsistencyCheckResult CheckEnvironmentVariablesConsistency(const UDeadlineCloudEnvironment* Env) ;
+	FParametersConsistencyCheckResult CheckEnvironmentVariablesConsistency(const UDeadlineCloudEnvironment* Env);
 
 	UFUNCTION()
 	void FixEnvironmentVariablesConsistency(UDeadlineCloudEnvironment* Env);
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h
new file mode 100644
index 00000000..8be9f190
--- /dev/null
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h
@@ -0,0 +1,41 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#pragma once
+#include "CoreMinimal.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h"
+
+using FValidatorFunc = TFunction<bool(const FString&, FText&)>;
+
+class FDeadlineCloudInputValidationHelper
+{
+public:
+
+    static FOnVerifyTextChanged GetStringValidationFunction(EValueValidationType ValidationType);
+    static FOnVerifyTextChanged GetPathValidationFunction(EValueValidationType ValidationType);
+
+    /** Basic length validation */
+    static bool IsValidLength(const FString& InStr, int32 Min, int32 Max);
+
+    /** Length validation with error reporting */
+    static bool IsValidLength(const FString& InStr, int32 Min, int32 Max, FText& OutError, const FText& FieldName = FText::FromString(TEXT("Value")));
+
+    /** Disallows Cc (control) characters (C0/C1 Unicode ranges) */
+    static bool ContainsNoControlCharacters(const FString& InStr, const TSet<TCHAR>& ExcludeList);
+
+    /** Disallows control characters and provides error message */
+    static bool ContainsNoControlCharacters(const FString& InStr, FText& OutError,
+        const TSet<TCHAR>& ExcludeList, const FText& FieldName = FText::FromString(TEXT("Value")));
+
+    /** Validates identifier: starts with [A-Za-z_] and contains only [A-Za-z0-9_] */
+    static bool IsValidIdentifier(const FString& InStr);
+
+    /** Identifier with error message */
+    static bool IsValidIdentifier(const FString& InStr, FText& OutError, const FText& FieldName = FText::FromString(TEXT("Identifier")));
+
+private:
+
+    static FValidatorFunc CreateLengthValidator(int32 Min, int32 Max);
+    static FValidatorFunc CreateLengthAndIdentifierValidator(int32 Min, int32 Max);
+    static FValidatorFunc CreateLengthAndControlValidator(int32 Min, int32 Max, TSet<TCHAR> ExcludeList);
+
+};
\ No newline at end of file
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJob.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJob.h
index b4be2b21..9d7cc10e 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJob.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJob.h
@@ -10,6 +10,7 @@
 /**
  * All Deadline Cloud job settings container struct
  */
+
  /**
   * Deadline Cloud Job Shared Settings
   * Goes as part of FDeadlineCloudJobPresetStruct,
@@ -22,11 +23,11 @@ struct UNREALDEADLINECLOUDSERVICE_API FDeadlineCloudJobSharedSettingsStruct
 
 public:
     /** Job Name */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 0))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 0, CustomWidgetType = STRING, ValidationType = JobName))
     FString Name = "Untitled";
 
     /** Job description */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 1))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 1, CustomWidgetType = STRING, ValidationType = JobDescription))
     FString Description = "No description";
 
     /** Job initial state */
@@ -34,15 +35,15 @@ struct UNREALDEADLINECLOUDSERVICE_API FDeadlineCloudJobSharedSettingsStruct
     FString InitialState = "READY";
 
     /** Max number of failed tasks */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 3))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (ClampMin = 0, DisplayPriority = 3))
     int32 MaximumFailedTasksCount = 1;
 
     /** Maximum retries per task */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 4))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (ClampMin = 0, DisplayPriority = 4))
     int32 MaximumRetriesPerTask = 50;
 
     /** Job priority */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (DisplayPriority = 5))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Job Shared Settings", meta = (ClampMin = 0, ClampMax = 100, DisplayPriority = 5))
     int32 Priority = 50;
 };
 
@@ -70,20 +71,20 @@ struct UNREALDEADLINECLOUDSERVICE_API FDeadlineCloudHostRequirementsStruct
 	FString CPU_Architecture;
 
 	/** Required number of CPU cores */
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 0, ClampMax = 10000, DisplayName = "vCPUs", EditCondition = "!bRunOnAllWorkerNodes"))
-	FInt32Interval CPUs = FInt32Interval(0, 0);
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 1, ClampMax = 10000, DisplayName = "vCPUs", EditCondition = "!bRunOnAllWorkerNodes"))
+	FInt32Interval CPUs = FInt32Interval(1, 1);
 
 	/** Required amount of RAM */
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 0, ClampMax = 10000, DisplayName = "Memory (GiB)", EditCondition = "!bRunOnAllWorkerNodes"))
-	FInt32Interval Memory = FInt32Interval(0, 0);
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 1, ClampMax = 10000, DisplayName = "Memory (GiB)", EditCondition = "!bRunOnAllWorkerNodes"))
+	FInt32Interval Memory = FInt32Interval(1, 1);
 
 	/** Required numer of GPU */
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 0, ClampMax = 10000, DisplayName = "GPUs", EditCondition = "!bRunOnAllWorkerNodes"))
 	FInt32Interval GPUs = FInt32Interval(0, 0);
 
 	/** Required number of VRAM */
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 0, ClampMax = 10000, DisplayName = "GPU Memory (GiB)", EditCondition = "!bRunOnAllWorkerNodes"))
-	FInt32Interval GPU_Memory = FInt32Interval(0, 0);
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 1, ClampMax = 10000, DisplayName = "GPU Memory (GiB)", EditCondition = "!bRunOnAllWorkerNodes"))
+	FInt32Interval GPU_Memory = FInt32Interval(1, 1);
 
 	/** Required amount of scratch space */
 	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Host requirements", meta = (ClampMin = 0, ClampMax = 10000, DisplayName = "Scratch Space", EditCondition = "!bRunOnAllWorkerNodes"))
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h
index dcfb1ae2..9fe2566d 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudJobPresetDetailsCustomization.h
@@ -37,7 +37,7 @@ class FPropertyAvailabilityHandler
 	 * Adds check box in "Name" view widget for provided property row. Makes property value editable in MRQ view
 	 * @param PropertyRow property row interface
 	 */
-	void EnableInMovieRenderQueue(IDetailPropertyRow& PropertyRow) const;
+	void EnableInMovieRenderQueue(IDetailPropertyRow& PropertyRow, TSharedPtr<SWidget> CustomValueWidget = TSharedPtr<SWidget>()) const;
 
 	/**
 	 * Disables row for editing in Deadline Cloud job preset DataAsset @ref UDeadlineCloudJobPreset 
@@ -87,14 +87,14 @@ class FDeadlineCloudJobPresetDetailsCustomization : public IPropertyTypeCustomiz
 	 * Adds ability to mark property as overridable in Deadline Cloud DataAsset 
 	 * @param PropertyRow Unreal property row interface
 	 */
-	void CustomizeStructChildrenInAssetDetails(IDetailPropertyRow& PropertyRow) const;
+	void CustomizeStructChildrenInAssetDetails(IDetailPropertyRow& PropertyRow, TSharedPtr<SWidget> CustomValueWidget = TSharedPtr<SWidget>()) const;
 
 	/**
 	 * Adds ability to override default job setting value for Deadline Cloud MRQ job
 	 * @param PropertyRow Unreal property ro interface
 	 * @param Job Deadline Cloud MRQ job
 	 */
-	void CustomizeStructChildrenInMovieRenderQueue(IDetailPropertyRow& PropertyRow, UMoviePipelineDeadlineCloudExecutorJob* Job) const;
+	void CustomizeStructChildrenInMovieRenderQueue(IDetailPropertyRow& PropertyRow, UMoviePipelineDeadlineCloudExecutorJob* Job, TSharedPtr<SWidget> CustomValueWidget = TSharedPtr<SWidget>()) const;
 
 	// static bool IsResetToDefaultVisibleOverride(TSharedPtr<IPropertyHandle> PropertyHandle, UMoviePipelineDeadlineCloudExecutorJob* Job);
 	// static void ResetToDefaultOverride(TSharedPtr<IPropertyHandle> PropertyHandle, UMoviePipelineDeadlineCloudExecutorJob* Job);
@@ -120,6 +120,10 @@ class FDeadlineCloudAttachmentDetailsCustomization : public IPropertyTypeCustomi
 protected:
 	/** Handles overridden settings in UI */
 	TSharedPtr<FPropertyAvailabilityHandler> PropertyOverrideHandler;
+
+    bool IsPathsContainsErrors(TSharedPtr<IPropertyHandle> PropertyHandle) const;
+    EVisibility GetPathsErrorWidgetVisibility(TSharedPtr<IPropertyHandle> PropertyHandle) const;
+    EVisibility GetPathsDefaultWidgetVisibility(TSharedPtr<IPropertyHandle> PropertyHandle) const;
 };
 
 /**
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudStepDetails.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudStepDetails.h
index 7c92e1b3..3b44d33a 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudStepDetails.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/DeadlineCloudJobSettings/DeadlineCloudStepDetails.h
@@ -94,7 +94,7 @@ class FDeadlineCloudStepParameterListBuilder
 public:
 
     static TSharedRef<FDeadlineCloudStepParameterListBuilder> MakeInstance(
-        TSharedRef<IPropertyHandle> InPropertyHandle, EValueType Type
+        TSharedRef<IPropertyHandle> InPropertyHandle, EValueType Type, FString Name
     );
 
 	FDeadlineCloudStepParameterListBuilder(
@@ -109,6 +109,7 @@ class FDeadlineCloudStepParameterListBuilder
     void OnGenerateEntry(TSharedRef<IPropertyHandle> ElementProperty, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder) const;
 
     EValueType Type;
+	FString Name;
     TSharedPtr<IPropertyHandleArray> ArrayProperty;
 };
 
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/PythonAPILibraries/PythonYamlLibrary.h b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/PythonAPILibraries/PythonYamlLibrary.h
index 9ba72b03..28f000e7 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/PythonAPILibraries/PythonYamlLibrary.h
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/Public/PythonAPILibraries/PythonYamlLibrary.h
@@ -24,6 +24,17 @@ enum class EValueType : uint8
     PATH    UMETA(DisplayName = "Path")
 };
 
+UENUM(BlueprintType)
+enum class EValueValidationType : uint8
+{
+	Default,
+	JobName,
+	JobDescription,
+    JobParameterValue,
+	StepParameterValue,
+	EnvParameterValue
+};
+
 USTRUCT(BlueprintType)
 struct FParameterDefinition
 {
diff --git a/src/unreal_plugin/Source/UnrealDeadlineCloudService/UnrealDeadlineCloudService.Build.cs b/src/unreal_plugin/Source/UnrealDeadlineCloudService/UnrealDeadlineCloudService.Build.cs
index 9b255a25..3e564d8c 100644
--- a/src/unreal_plugin/Source/UnrealDeadlineCloudService/UnrealDeadlineCloudService.Build.cs
+++ b/src/unreal_plugin/Source/UnrealDeadlineCloudService/UnrealDeadlineCloudService.Build.cs
@@ -24,18 +24,19 @@ public UnrealDeadlineCloudService(ReadOnlyTargetRules Target) : base(Target)
         PublicDependencyModuleNames.AddRange(
             new string[]
             {
-                    "UnrealEd",
-    "EditorSubsystem",
-             "Slate",
-             "SlateCore",
-             "EditorWidgets",
-             "Core",
-             "CoreUObject",
-             "Engine",
-             "MovieRenderPipelineCore",
+                 "AutomationDriver",
+                 "UnrealEd",
+                 "EditorSubsystem",
+                 "Slate",
+                 "SlateCore",
+                 "EditorWidgets",
+                 "Core",
+                 "CoreUObject",
+                 "Engine",
+                 "MovieRenderPipelineCore",
                  "MovieRenderPipelineEditor",
-             "InputCore",
-             "DesktopWidgets",
+                 "InputCore",
+                 "DesktopWidgets",
             }
             );
 


Reviews:
user: login: baxeaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T20:28:17Z
user: login: baxeaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T21:32:44Z
user: login: baxeaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:22:09Z
user: login: karthikbekalp
body: I agree with comments from @baxeaz but there's nothing extra for me to add. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:46:48Z
user: login: baxeaz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:48:52Z

Review comments:
diff_hunk: @@ -0,0 +1,223 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
+//#include "Internationalization/Regex.h"
path: src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp
user: login: baxeaz
body: No longer needed?
created_at: 2025-05-08T20:28:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,223 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
+//#include "Internationalization/Regex.h"
+
+#define LOCTEXT_NAMESPACE "DeadlineWidgets"
+
+
+//TODO: Refactor this class to use OpenJD validators
+//The best approach to validation is to create a wrapper around the OpenJD validators to avoid 
+//code duplication and reduce the chance of errors. However, for now, private functions are used internally
+
+FOnVerifyTextChanged FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType ValidationType)
+{
+    switch (ValidationType)
+    {
+        using enum EValueValidationType;
+
+    case EValueValidationType::JobName:
+    {
+        return FOnVerifyTextChanged::CreateLambda(
+            [](const FText Input, FText& Error) -> bool
+            {
+                FString InputString = Input.ToString();
+                if (!IsValidLength(InputString, 1, 64, Error))
path: src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp
user: login: baxeaz
body: I'm not sure this is being executed as expected - In testing I'm able to set a job name with an arbitrarily long length and submit it
created_at: 2025-05-08T21:19:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,47 +1,247 @@
 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 
 #include "DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.h"
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
 #include "Widgets/Input/SFilePathPicker.h"
 #include "DetailLayoutBuilder.h"
 #include "Widgets/Input/SNumericEntryBox.h"
 #include "EditorDirectories.h"
+#include "Widgets/Notifications/SPopUpErrorText.h"
+#include "DesktopPlatformModule.h"
 
 #define LOCTEXT_NAMESPACE "DeadlineWidgets"
+
 /*
 SDeadlineCloudFilePathWidget is a custom Slate widget class that implements a file path picker interface.
  */
 class  SDeadlineCloudFilePathWidget : public SCompoundWidget
 {
 public:
-    SLATE_BEGIN_ARGS(SDeadlineCloudFilePathWidget) {}
+    SLATE_BEGIN_ARGS(SDeadlineCloudFilePathWidget) 
+		: _BrowseButtonToolTip(LOCTEXT("BrowseButtonToolTip", "Choose a file from this computer"))
+		, _FileTypeFilter(TEXT("All files (*.*)|*.*"))
+		, _Font()
+		, _IsReadOnly(false)
+		, _DialogReturnsFullPath(false)
+		{}
         SLATE_ARGUMENT(TSharedPtr<IPropertyHandle>, PathPropertyHandle)
+		SLATE_EVENT(FOnVerifyTextChanged, IsValidInput)
path: src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudDetailsWidgetsHelper.cpp
user: login: baxeaz
body: Minor - lots of varying indentation/formatting in this review
created_at: 2025-05-08T21:26:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,223 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+#include "DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.h"
+//#include "Internationalization/Regex.h"
+
+#define LOCTEXT_NAMESPACE "DeadlineWidgets"
+
+
+//TODO: Refactor this class to use OpenJD validators
+//The best approach to validation is to create a wrapper around the OpenJD validators to avoid 
+//code duplication and reduce the chance of errors. However, for now, private functions are used internally
+
+FOnVerifyTextChanged FDeadlineCloudInputValidationHelper::GetStringValidationFunction(EValueValidationType ValidationType)
+{
+    switch (ValidationType)
+    {
+        using enum EValueValidationType;
+
+    case EValueValidationType::JobName:
+    {
+        return FOnVerifyTextChanged::CreateLambda(
+            [](const FText Input, FText& Error) -> bool
+            {
+                FString InputString = Input.ToString();
+                if (!IsValidLength(InputString, 1, 64, Error))
+                {
+                    return false;
+                }
+
+				if (!IsValidIdentifier(InputString, Error))
+				{
+					return false;
+				}
+
+                return true;
+            });
+    }
+
+    case EValueValidationType::JobDescription:
+    {
+        return FOnVerifyTextChanged::CreateLambda(
+            [](const FText Input, FText& Error) -> bool
path: src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/DeadlineCloudJobSettings/DeadlineCloudInputValidationHelper.cpp
user: login: baxeaz
body: Can Input in all of these Lambdas be a const FText& to save a copy?  Looks like we're taking a local copy below already with ToString anyway
created_at: 2025-05-08T21:29:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -464,7 +464,7 @@ ULevelSequence* FindFirstLevelSequence()
     return ShortestPathSequence;
 }
 
-IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieQueueCreateJobTest, "Deadline.Integration.CreateJob",
path: src/unreal_plugin/Source/UnrealDeadlineCloudService/Private/Tests/Integration/DeadlinePluginTest_CreateJobTest.cpp
user: login: baxeaz
body: Changing this means we also need to update the end to end tests (test/end_to_end/conftest.py is just a comment, then test_create_job.py and test_worker_agent.py try to run the test)
created_at: 2025-05-14T15:22:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-14T07:47:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [8 New issues](https://sonarcloud.io/project/issues?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aws-deadline_deadline-cloud-for-unreal-engine&pullRequest=145)


author_association: NONE
user: login: baxeaz
created_at: 2025-05-14T18:49:24Z
body: Going to go ahead and merge and just update the e2e tests in a follow up :)
author_association: CONTRIBUTOR