diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
index f59a967b1efef7c004b88492d98982634a980741..f5500e274396aa4dad4376251461ab422a0ae613 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -38,6 +38,7 @@ module Gitlab
 
           def initialize(*)
             super
+
             @global = self
           end
 
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb
index 6a559ee8c04455bcc87cdd96ebd5a102c85ccf5c..073044b66f89c66ae593d1042b22214b6d901bfc 100644
--- a/lib/gitlab/ci/config/node/hidden_job.rb
+++ b/lib/gitlab/ci/config/node/hidden_job.rb
@@ -10,6 +10,7 @@ module Gitlab
 
           validations do
             validates :config, type: Hash
+            validates :config, presence: true
           end
 
           def relevant?
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
index 4a9cc28d763030f68600087f26b6ceffb8d2b83f..28a3f4076057b6b0aaa91b3cb5e53ff3f4fab32b 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -8,6 +8,10 @@ module Gitlab
         class Job < Entry
           include Configurable
 
+          validations do
+            validates :config, presence: true
+          end
+
           node :stage, Stage,
             description: 'Pipeline stage this job will be executed into.'
 
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index f6acc25e4fb270ef071518d51b6084325a9eaa8a..7a164b69aff67ef52762cd175b19964317792a92 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -30,17 +30,19 @@ module Gitlab
           private
 
           def create(name, config)
-            job_node(name).new(config, job_attributes(name))
+            Node::Factory.new(job_node(name))
+              .value(config || {})
+              .with(key: name, parent: self, global: @global)
+              .with(description: "#{name} job definition.")
+              .create!
           end
 
           def job_node(name)
-            name.to_s.start_with?('.') ? Node::HiddenJob : Node::Job
-          end
-
-          def job_attributes(name)
-            @attributes.merge(key: name,
-                              parent: self,
-                              description: "#{name} job definition.")
+            if name.to_s.start_with?('.')
+              Node::HiddenJob
+            else
+              Node::Job
+            end
           end
         end
       end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
index 10e5f05a2d5cee52f1edcc02a4cb92736f5188c9..3e1c197fe61d7c0eec878f37526ff6bb8a239639 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -137,7 +137,7 @@ describe Gitlab::Ci::Config::Node::Global do
     end
 
     context 'when most of entires not defined' do
-      let(:hash) { { cache: { key: 'a' }, rspec: {} } }
+      let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
       before { global.process! }
 
       describe '#nodes' do
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
index ab865c3522eec3b5c55c2c96e613c35ca1d4ac45..cc44e2cc05448e3c3b9e4b1da069e609c1fb8ec7 100644
--- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
@@ -31,6 +31,16 @@ describe Gitlab::Ci::Config::Node::HiddenJob do
           end
         end
       end
+
+      context 'when config is empty' do
+        let(:config) { {} }
+
+        describe '#valid' do
+          it 'is invalid' do
+            expect(entry).not_to be_valid
+          end
+        end
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
index 2a4296448fb74fe048a613cdd276b565247b010b..f841936ee6b6bb620d9e60af5d2c4ebd66fab05c 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -39,6 +39,16 @@ describe Gitlab::Ci::Config::Node::Job do
           end
         end
       end
+
+      context 'when config is empty' do
+        let(:config) { {} }
+
+        describe '#valid' do
+          it 'is invalid' do
+            expect(entry).not_to be_valid
+          end
+        end
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index 52018958dcf210a0ab6c4193ea158048156b5915..b0171174157924f3c6068356e068a604f768ae47 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -4,6 +4,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
   let(:entry) { described_class.new(config, global: spy) }
 
   describe 'validations' do
+    before do
+      entry.process!
+      entry.validate!
+    end
+
     context 'when entry config value is correct' do
       let(:config) { { rspec: { script: 'rspec' } } }
 
@@ -25,25 +30,20 @@ describe Gitlab::Ci::Config::Node::Jobs do
           end
         end
 
-        context 'when no visible jobs present' do
-          let(:config) { { '.hidden'.to_sym => {} } }
+        context 'when job is unspecified' do
+          let(:config) { { rspec: nil } }
 
-          context 'when not processed' do
-            it 'is valid' do
-              expect(entry.errors).to be_empty
-            end
+          it 'is not valid' do
+            expect(entry).not_to be_valid
           end
+        end
 
-          context 'when processed' do
-            before do
-              entry.process!
-              entry.validate!
-            end
+        context 'when no visible jobs present' do
+          let(:config) { { '.hidden'.to_sym => { script: [] } } }
 
-            it 'returns error about no visible jobs defined' do
-              expect(entry.errors)
-                .to include 'jobs config should contain at least one visible job'
-            end
+          it 'returns error about no visible jobs defined' do
+            expect(entry.errors)
+              .to include 'jobs config should contain at least one visible job'
           end
         end
       end