diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 5ff85b568796c3ea719dc0d0b9436bb6a0ceef45..d9a262cc1b776179c872306c951b24bc3ad62f0c 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -6213,6 +6213,30 @@ Input type: `DestroySnippetInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
| `snippet` | [`Snippet`](#snippet) | Snippet after mutation. |
+### `Mutation.devfileValidate`
+
+{{< details >}}
+**Introduced** in GitLab 18.4.
+**Status**: Experiment.
+{{< /details >}}
+
+Input type: `DevfileValidateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `devfileYaml` | [`String!`](#string) | Input devfile. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
+| `valid` | [`Boolean`](#boolean) | Status whether devfile is valid or not. |
+
### `Mutation.disableDevopsAdoptionNamespace`
**Status**: Beta.
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index eaf2ff2e2066b2920ecac9cc9845c6b122099337..3b057ca1530acd47c1f96ea4f365270262a71fae 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -201,6 +201,7 @@ def self.authorization_scopes
mount_mutation ::Mutations::Ci::Runners::ExportUsage
mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Create
mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Update
+ mount_mutation ::Mutations::RemoteDevelopment::DevfileOperations::Validate, experiment: { milestone: '18.4' }
mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Create
mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Delete
mount_mutation ::Mutations::RemoteDevelopment::OrganizationClusterAgentMappingOperations::Create, experiment: {
diff --git a/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb b/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0221524388311d29c285a711f73acb28d34713ab
--- /dev/null
+++ b/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Mutations
+ module RemoteDevelopment
+ module DevfileOperations
+ class Validate < BaseMutation
+ graphql_name "DevfileValidate"
+
+ argument :devfile_yaml,
+ GraphQL::Types::String,
+ required: true,
+ description: "Input devfile."
+
+ field :valid,
+ GraphQL::Types::Boolean,
+ description: "Status whether devfile is valid or not."
+
+ # @param [String] devfile_yaml
+ # @return [Hash]
+ def resolve(devfile_yaml)
+ unless License.feature_available?(:remote_development)
+ raise_resource_not_available_error!("'remote_development' licensed feature is not available")
+ end
+
+ yaml_content = devfile_yaml.is_a?(Hash) ? devfile_yaml[:devfile_yaml] : devfile_yaml
+
+ domain_main_class_args = {
+ devfile_yaml: yaml_content,
+ user: current_user
+ }
+
+ response = ::RemoteDevelopment::CommonService.execute(
+ domain_main_class: ::RemoteDevelopment::DevfileOperations::Main,
+ domain_main_class_args: domain_main_class_args
+ )
+
+ {
+ valid: response.success?,
+ errors: response.error? ? response.message.split(", ") : []
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb b/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6199f6a8ec3a33a5cbe5e0874f8b9c21d78c8307
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+# noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot
+RSpec.describe "Validating a devfile", feature_category: :workspaces do
+ include GraphqlHelpers
+ include StubFeatureFlags
+ include_context "with remote development shared fixtures"
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { user }
+ let(:devfile_yaml) { " " }
+
+ let(:mutation) do
+ graphql_mutation(:devfile_validate, mutation_args)
+ end
+
+ let(:all_mutation_args) do
+ {
+ devfile_yaml: devfile_yaml
+ }
+ end
+
+ let(:mutation_args) { all_mutation_args }
+
+ let(:expected_service_args) do
+ {
+ domain_main_class: ::RemoteDevelopment::DevfileOperations::Main,
+ domain_main_class_args: {
+ devfile_yaml: devfile_yaml,
+ user: current_user
+ }
+ }
+ end
+
+ def mutation_response
+ graphql_mutation_response(:devfile_validate)
+ end
+
+ before do
+ stub_licensed_features(remote_development: true)
+ end
+
+ context "when devfile is valid" do
+ let(:stub_service_response) { ServiceResponse.success(message: "Validation Success") }
+
+ before do
+ allow(RemoteDevelopment::CommonService).to receive(:execute).with(expected_service_args) do
+ stub_service_response
+ end
+ end
+
+ it "returns empty array and valid status" do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect_graphql_errors_to_be_empty
+ expect(mutation_response["valid"]).to be true
+ end
+ end
+
+ context "when a devfile is invalid" do
+ let(:stub_service_response) { ServiceResponse.error(message: "Validation Error", reason: :bad_request) }
+
+ before do
+ allow(RemoteDevelopment::CommonService).to receive(:execute).with(expected_service_args) do
+ stub_service_response
+ end
+ end
+
+ it_behaves_like "a mutation that returns errors in the response", errors: ["Validation Error"]
+
+ it "asserts valid key is false" do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response["valid"]).to be false
+ end
+ end
+
+ context "when devfile argument is missing" do
+ let(:mutation_args) { all_mutation_args.except(:devfile_yaml) }
+
+ it "returns error about required argument" do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/provided invalid value for devfileYaml \(Expected value to not be null\)/)
+ end
+ end
+
+ context "when remote_development feature is unlicensed" do
+ before do
+ stub_licensed_features(remote_development: false)
+ end
+
+ it_behaves_like "a mutation that returns top-level errors" do
+ let(:match_errors) { include(/'remote_development' licensed feature is not available/) }
+ end
+ end
+end