diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index fcbe0b350a3f6315d6c1a230a10fb32d022605c4..279d3faa110a6c44793144bcf7c940b4dd866ab4 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1031,6 +1031,13 @@ def debug_mode?
       variables.any? { |variable| variable[:key] == 'CI_DEBUG_TRACE' && variable[:value].casecmp('true') == 0 }
     end
 
+    def drop_with_exit_code!(failure_reason, exit_code)
+      transaction do
+        conditionally_allow_failure!(exit_code)
+        drop!(failure_reason)
+      end
+    end
+
     protected
 
     def run_status_commit_hooks!
@@ -1114,6 +1121,22 @@ def job_jwt_variables
         Gitlab::ErrorTracking.track_exception(e)
       end
     end
+
+    def conditionally_allow_failure!(exit_code)
+      return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+      return unless exit_code
+
+      if allowed_to_fail_with_code?(exit_code)
+        update_columns(allow_failure: true)
+      end
+    end
+
+    def allowed_to_fail_with_code?(exit_code)
+      options
+        .dig(:allow_failure_criteria, :exit_codes)
+        .to_a
+        .include?(exit_code)
+    end
   end
 end
 
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
index f01d41d94141af20b472b0d559fee19fe9466d08..874f4bf459a0e6d3930bd3b5469beb5776c5dfc1 100644
--- a/app/services/ci/update_build_state_service.rb
+++ b/app/services/ci/update_build_state_service.rb
@@ -111,7 +111,7 @@ def update_build_state!
 
         Result.new(status: 200)
       when 'failed'
-        build.drop!(params[:failure_reason] || :unknown_failure)
+        build.drop_with_exit_code!(params[:failure_reason] || :unknown_failure, params[:exit_code])
 
         Result.new(status: 200)
       else
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 86e1a939df1cd00f466c2a7aa1e65f4f0b3148ad..5cfb65e1fbb5f6b243f64382f9d3aeeb1094a62f 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -180,6 +180,7 @@ class Runner < ::API::Base
             optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum)
             optional :bytesize, type: Integer, desc: %q(Job's trace size in bytes)
           end
+          optional :exit_code, type: Integer, desc: %q(Job's exit code)
         end
         put '/:id' do
           job = authenticate_job!
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7e52b897dbc4d5e21206e1fb73651d02b81cff2b..b2473b01cf43c630aa5275a1bc5a6753a0b3b41d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4823,4 +4823,107 @@ def run_job_without_exception
       it { is_expected.to eq false }
     end
   end
+
+  describe '#drop_with_exit_code!' do
+    let(:exit_code) { 1 }
+    let(:options) { {} }
+
+    before do
+      build.options.merge!(options)
+      build.save!
+    end
+
+    subject(:drop_with_exit_code) do
+      build.drop_with_exit_code!(:unknown_failure, exit_code)
+    end
+
+    shared_examples 'drops the build without changing allow_failure' do
+      it 'does not change allow_failure' do
+        expect { drop_with_exit_code }
+          .not_to change { build.reload.allow_failure }
+      end
+
+      it 'drops the build' do
+        expect { drop_with_exit_code }
+          .to change { build.reload.failed? }
+      end
+    end
+
+    context 'when exit_codes are not defined' do
+      it_behaves_like 'drops the build without changing allow_failure'
+    end
+
+    context 'when allow_failure_criteria is nil' do
+      let(:options) { { allow_failure_criteria: nil } }
+
+      it_behaves_like 'drops the build without changing allow_failure'
+    end
+
+    context 'when exit_codes is nil' do
+      let(:options) do
+        {
+          allow_failure_criteria: {
+            exit_codes: nil
+          }
+        }
+      end
+
+      it_behaves_like 'drops the build without changing allow_failure'
+    end
+
+    context 'when exit_codes do not match' do
+      let(:options) do
+        {
+          allow_failure_criteria: {
+            exit_codes: [2, 3, 4]
+          }
+        }
+      end
+
+      it_behaves_like 'drops the build without changing allow_failure'
+    end
+
+    context 'with matching exit codes' do
+      let(:options) do
+        { allow_failure_criteria: { exit_codes: [1, 2, 3] } }
+      end
+
+      it 'changes allow_failure' do
+        expect { drop_with_exit_code }
+          .to change { build.reload.allow_failure }
+      end
+
+      it 'drops the build' do
+        expect { drop_with_exit_code }
+          .to change { build.reload.failed? }
+      end
+
+      it 'is executed inside a transaction' do
+        expect(build).to receive(:drop!)
+          .with(:unknown_failure)
+          .and_raise(ActiveRecord::Rollback)
+
+        expect(build).to receive(:conditionally_allow_failure!)
+          .with(1)
+          .and_call_original
+
+        expect { drop_with_exit_code }
+          .not_to change { build.reload.allow_failure }
+      end
+
+      context 'when exit_code is nil' do
+        let(:exit_code) {}
+
+        it_behaves_like 'drops the build without changing allow_failure'
+      end
+
+      context 'when ci_allow_failure_with_exit_codes is disabled' do
+        before do
+          stub_feature_flags(ci_allow_failure_with_exit_codes: false)
+        end
+
+        it_behaves_like 'drops the build without changing allow_failure'
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index e9d793d5a22f9fb34567168779d3331c6567f7dd..f4c99307b1ab757f60b16a8f8026a97973d4510e 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -78,6 +78,33 @@
           end
         end
 
+        context 'when an exit_code is provided' do
+          context 'when the exit_codes are acceptable' do
+            before do
+              job.options[:allow_failure_criteria] = { exit_codes: [1] }
+              job.save!
+            end
+
+            it 'accepts an exit code' do
+              update_job(state: 'failed', exit_code: 1)
+
+              expect(job.reload).to be_failed
+              expect(job.allow_failure).to be_truthy
+              expect(job).to be_unknown_failure
+            end
+          end
+
+          context 'when the exit_codes are not defined' do
+            it 'ignore the exit code' do
+              update_job(state: 'failed', exit_code: 1)
+
+              expect(job.reload).to be_failed
+              expect(job.allow_failure).to be_falsy
+              expect(job).to be_unknown_failure
+            end
+          end
+        end
+
         context 'when failure_reason is script_failure' do
           before do
             update_job(state: 'failed', failure_reason: 'script_failure')
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 3112e5dda1b6a90e7877d93d91ea5a2b3875c012..63190cc5d49a7a89aa88a7ce451d05240f3fb686 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -82,8 +82,9 @@
     let(:params) do
       {
         output: { checksum: 'crc32:12345678', bytesize: 123 },
+        state: 'failed',
         failure_reason: 'script_failure',
-        state: 'failed'
+        exit_code: 42
       }
     end
 
@@ -95,6 +96,15 @@
         expect(result.status).to eq 200
       end
 
+      it 'updates the allow_failure flag' do
+        expect(build)
+          .to receive(:drop_with_exit_code!)
+          .with('script_failure', 42)
+          .and_call_original
+
+        subject.execute
+      end
+
       it 'does not increment invalid trace metric' do
         execute_with_stubbed_metrics!
 
@@ -115,6 +125,15 @@
         expect(build).to be_failed
       end
 
+      it 'updates the allow_failure flag' do
+        expect(build)
+          .to receive(:drop_with_exit_code!)
+          .with('script_failure', 42)
+          .and_call_original
+
+        subject.execute
+      end
+
       it 'responds with 200 OK status' do
         result = subject.execute