diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 7148f4c2a79e1e3f400bc7088f46d5becb6010ca..fa6569e8bb26faa6008c60fa4fa0cf7b7c88df08 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -54,8 +54,11 @@ module Gitlab
             if leaf?
               @config
             else
-              defined = @nodes.select { |_key, value| value.defined? }
-              Hash[defined.map { |key, node| [key, node.value] }]
+              meaningful = @nodes.select do |_key, value|
+                value.defined? && value.relevant?
+              end
+
+              Hash[meaningful.map { |key, node| [key, node.value] }]
             end
           end
 
@@ -63,6 +66,10 @@ module Gitlab
             true
           end
 
+          def relevant?
+            true
+          end
+
           def self.default
           end
 
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6a559ee8c04455bcc87cdd96ebd5a102c85ccf5c
--- /dev/null
+++ b/lib/gitlab/ci/config/node/hidden_job.rb
@@ -0,0 +1,22 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        ##
+        # Entry that represents a hidden CI/CD job.
+        #
+        class HiddenJob < Entry
+          include Validatable
+
+          validations do
+            validates :config, type: Hash
+          end
+
+          def relevant?
+            false
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index 915b46652f2a5cde54f44fe93379c8bd35c76125..a76b7a260c424c2a025ee1e6f0c981718aa4f6f7 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -19,12 +19,20 @@ module Gitlab
           private
 
           def create_node(key, essence)
-            Node::Job.new(essence).tap do |job|
+            fabricate_job(key, essence).tap do |job|
               job.key = key
               job.parent = self
               job.description = "#{key} job definition."
             end
           end
+
+          def fabricate_job(key, essence)
+            if key.to_s.start_with?('.')
+              Node::HiddenJob.new(essence)
+            else
+              Node::Job.new(essence)
+            end
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb
index 758a6cf435672d50610df01516211ad45f73d198..dcfeb1943745d4b13c1fcaa35d0145376573456e 100644
--- a/lib/gitlab/ci/config/node/validator.rb
+++ b/lib/gitlab/ci/config/node/validator.rb
@@ -31,7 +31,7 @@ module Gitlab
 
           def location
             predecessors = ancestors.map(&:key).compact
-            current = key || @node.class.name.demodulize.underscore
+            current = key || @node.class.name.demodulize.underscore.humanize
             predecessors.append(current).join(':')
           end
         end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab865c3522eec3b5c55c2c96e613c35ca1d4ac45
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::HiddenJob do
+  let(:entry) { described_class.new(config) }
+
+  describe 'validations' do
+    context 'when entry config value is correct' do
+      let(:config) { { image: 'ruby:2.2' } }
+
+      describe '#value' do
+        it 'returns key value' do
+          expect(entry.value).to eq(image: 'ruby:2.2')
+        end
+      end
+
+      describe '#valid?' do
+        it 'is valid' do
+          expect(entry).to be_valid
+        end
+      end
+    end
+
+    context 'when entry value is not correct' do
+      context 'incorrect config value type' do
+        let(:config) { ['incorrect'] }
+
+        describe '#errors' do
+          it 'saves errors' do
+            expect(entry.errors)
+              .to include 'hidden job config should be a hash'
+          end
+        end
+      end
+    end
+  end
+
+  describe '#leaf?' do
+    it 'is a leaf' do
+      expect(entry).to be_leaf
+    end
+  end
+
+  describe '#relevant?' do
+    it 'is not a relevant entry' do
+      expect(entry).not_to be_relevant
+    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 7dd25a23c83d09a3274c4f7091029242995af51a..15c7f9bc39476f79b53d24f5964615c01508f72f 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -33,4 +33,10 @@ describe Gitlab::Ci::Config::Node::Job do
       end
     end
   end
+
+  describe '#relevant?' do
+    it 'is a relevant entry' do
+      expect(entry).to be_relevant
+    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 7f80e11cea37193de5e4b7f2230d73c4b210fb0d..b2d2a92d9e8df5f1fd2edf046268f2752a14dd28 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -36,18 +36,29 @@ describe Gitlab::Ci::Config::Node::Jobs do
     end
   end
 
-  describe '#descendants' do
+  context 'when valid job entries processed' do
     before { entry.process! }
 
     let(:config) do
       { rspec: { script: 'rspec' },
-        spinach: { script: 'spinach' } }
+        spinach: { script: 'spinach' },
+        '.hidden'.to_sym => {} }
     end
 
-    it 'creates two descendant nodes' do
-      expect(entry.descendants.count).to eq 2
-      expect(entry.descendants)
-        .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
+    describe '#descendants' do
+      it 'creates valid descendant nodes' do
+        expect(entry.descendants.count).to eq 3
+        expect(entry.descendants.first(2))
+          .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
+        expect(entry.descendants.last)
+          .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
+      end
+    end
+
+    describe '#value' do
+      it 'returns value of visible jobs only' do
+        expect(entry.value.keys).to eq [:rspec, :spinach]
+      end
     end
   end
 end