diff --git a/app/models/champ.rb b/app/models/champ.rb
index 1f0741e3f..7771007c6 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -10,12 +10,13 @@ class Champ < ApplicationRecord
   has_many :geo_areas, dependent: :destroy
   belongs_to :etablissement, dependent: :destroy
 
-  delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, to: :type_de_champ
+  delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, to: :type_de_champ
 
   scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }
   scope :public_only, -> { where(private: false) }
   scope :private_only, -> { where(private: true) }
-  scope :ordered, -> { includes(:type_de_champ).order('types_de_champ.order_place') }
+  scope :ordered, -> { includes(:type_de_champ).order(:row, 'types_de_champ.order_place') }
+  scope :root, -> { where(parent_id: nil) }
 
   def public?
     !private?
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
new file mode 100644
index 000000000..3ad10bd7f
--- /dev/null
+++ b/app/models/champs/repetition_champ.rb
@@ -0,0 +1,13 @@
+class Champs::RepetitionChamp < Champ
+  has_many :champs, -> { ordered }, foreign_key: :parent_id, dependent: :destroy
+
+  accepts_nested_attributes_for :champs, allow_destroy: true
+
+  def rows
+    champs.group_by(&:row).values
+  end
+
+  def search_terms
+    # The user cannot enter any information here so it doesn’t make much sense to search
+  end
+end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index 436c8c707..f0849579c 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -18,8 +18,8 @@ class Dossier < ApplicationRecord
   has_one :attestation, dependent: :destroy
 
   has_many :pieces_justificatives, dependent: :destroy
-  has_many :champs, -> { public_only.ordered }, dependent: :destroy
-  has_many :champs_private, -> { private_only.ordered }, class_name: 'Champ', dependent: :destroy
+  has_many :champs, -> { root.public_only.ordered }, dependent: :destroy
+  has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', dependent: :destroy
   has_many :commentaires, dependent: :destroy
   has_many :invites, dependent: :destroy
   has_many :follows
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 61bf18695..10b0af8bc 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -28,11 +28,15 @@ class TypeDeChamp < ApplicationRecord
     dossier_link: 'dossier_link',
     piece_justificative: 'piece_justificative',
     siret: 'siret',
-    carte: 'carte'
+    carte: 'carte',
+    repetition: 'repetition'
   }
 
   belongs_to :procedure
 
+  belongs_to :parent, class_name: 'TypeDeChamp'
+  has_many :types_de_champ, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy
+
   store :options, accessors: [:cadastres, :quartiers_prioritaires, :parcelles_agricoles]
 
   after_initialize :set_dynamic_type
@@ -105,7 +109,25 @@ class TypeDeChamp < ApplicationRecord
   end
 
   def non_fillable?
-    type_champ.in?([TypeDeChamp.type_champs.fetch(:header_section), TypeDeChamp.type_champs.fetch(:explication)])
+    type_champ.in?([
+      TypeDeChamp.type_champs.fetch(:header_section),
+      TypeDeChamp.type_champs.fetch(:explication)
+    ])
+  end
+
+  def exclude_from_export?
+    type_champ.in?([
+      TypeDeChamp.type_champs.fetch(:header_section),
+      TypeDeChamp.type_champs.fetch(:explication),
+      TypeDeChamp.type_champs.fetch(:repetition)
+    ])
+  end
+
+  def exclude_from_view?
+    type_champ.in?([
+      TypeDeChamp.type_champs.fetch(:explication),
+      TypeDeChamp.type_champs.fetch(:repetition)
+    ])
   end
 
   def public?
diff --git a/app/models/types_de_champ/repetition_type_de_champ.rb b/app/models/types_de_champ/repetition_type_de_champ.rb
new file mode 100644
index 000000000..5abf8efd3
--- /dev/null
+++ b/app/models/types_de_champ/repetition_type_de_champ.rb
@@ -0,0 +1,2 @@
+class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
+end
diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb
index f550ad81d..b50c6c6f2 100644
--- a/app/services/procedure_export_service.rb
+++ b/app/services/procedure_export_service.rb
@@ -142,10 +142,10 @@ class ProcedureExportService
     headers = ATTRIBUTES.map do |key|
       label_for_export(key.to_s)
     end
-    headers += @procedure.types_de_champ.ordered.map do |champ|
+    headers += @procedure.types_de_champ.ordered.reject(&:exclude_from_export?).map do |champ|
       label_for_export(champ.libelle)
     end
-    headers += @procedure.types_de_champ_private.ordered.map do |champ|
+    headers += @procedure.types_de_champ_private.ordered.reject(&:exclude_from_export?).map do |champ|
       label_for_export(champ.libelle)
     end
     headers += ETABLISSEMENT_ATTRIBUTES.map do |key|
@@ -184,10 +184,10 @@ class ProcedureExportService
         end
       end
       values = normalize_values(values)
-      values += dossier.champs.map do |champ|
+      values += dossier.champs.reject(&:exclude_from_export?).map do |champ|
         value_for_export(champ)
       end
-      values += dossier.champs_private.map do |champ|
+      values += dossier.champs_private.reject(&:exclude_from_export?).map do |champ|
         value_for_export(champ)
       end
       values += etablissement_data(dossier.etablissement)
diff --git a/app/services/types_de_champ_service.rb b/app/services/types_de_champ_service.rb
index 43a28b5ed..794a13afa 100644
--- a/app/services/types_de_champ_service.rb
+++ b/app/services/types_de_champ_service.rb
@@ -3,7 +3,8 @@ class TypesDeChampService
 
   TOGGLES = {
     TypeDeChamp.type_champs.fetch(:siret)                 => :champ_siret?,
-    TypeDeChamp.type_champs.fetch(:integer_number)        => :champ_integer_number?
+    TypeDeChamp.type_champs.fetch(:integer_number)        => :champ_integer_number?,
+    TypeDeChamp.type_champs.fetch(:repetition)            => :champ_repetition?
   }
 
   def options
diff --git a/app/views/shared/dossiers/_champs.html.haml b/app/views/shared/dossiers/_champs.html.haml
index b7feb25a9..928efe27c 100644
--- a/app/views/shared/dossiers/_champs.html.haml
+++ b/app/views/shared/dossiers/_champs.html.haml
@@ -1,6 +1,6 @@
 %table.table.vertical.dossier-champs
   %tbody
-    - champs.reject { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:explication) }.each do |c|
+    - champs.reject(&:exclude_from_view?).each do |c|
       %tr
         - case c.type_champ
         - when TypeDeChamp.type_champs.fetch(:header_section)
diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml
new file mode 100644
index 000000000..b9fcfa711
--- /dev/null
+++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml
@@ -0,0 +1 @@
+%h2.repetition-libelle= champ.libelle
diff --git a/config/features.rb b/config/features.rb
index 48cc54e1d..d998c2ee0 100644
--- a/config/features.rb
+++ b/config/features.rb
@@ -11,6 +11,8 @@ Flipflop.configure do
       title: "Champ SIRET"
     feature :champ_integer_number,
       title: "Champ nombre entier"
+    feature :champ_repetition,
+      title: "Bloc répétable (NE MARCHE PAS – NE PAS ACTIVER)"
   end
 
   feature :web_hook
diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml
index 642e43579..a76f1ad6b 100644
--- a/config/locales/models/type_de_champ/fr.yml
+++ b/config/locales/models/type_de_champ/fr.yml
@@ -31,3 +31,4 @@ fr:
           piece_justificative: 'Pièce justificative'
           siret: 'SIRET'
           carte: 'Carte'
+          repetition: 'Bloc répétable'
diff --git a/db/migrate/20181217125100_create_champ_groups.rb b/db/migrate/20181217125100_create_champ_groups.rb
new file mode 100644
index 000000000..ac55a12e8
--- /dev/null
+++ b/db/migrate/20181217125100_create_champ_groups.rb
@@ -0,0 +1,15 @@
+class CreateChampGroups < ActiveRecord::Migration[5.2]
+  def change
+    add_column :types_de_champ, :parent_id, :bigint
+    add_index :types_de_champ, :parent_id
+
+    add_column :champs, :parent_id, :bigint
+    add_index :champs, :parent_id
+
+    add_column :champs, :row, :integer
+    add_index :champs, :row
+
+    add_foreign_key :types_de_champ, :types_de_champ, column: :parent_id
+    add_foreign_key :champs, :champs, column: :parent_id
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 626f70db6..2a8ec1ccc 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -153,8 +153,12 @@ ActiveRecord::Schema.define(version: 2018_12_18_204707) do
     t.datetime "updated_at"
     t.boolean "private", default: false, null: false
     t.integer "etablissement_id"
+    t.bigint "parent_id"
+    t.integer "row"
     t.index ["dossier_id"], name: "index_champs_on_dossier_id"
+    t.index ["parent_id"], name: "index_champs_on_parent_id"
     t.index ["private"], name: "index_champs_on_private"
+    t.index ["row"], name: "index_champs_on_row"
     t.index ["type_de_champ_id"], name: "index_champs_on_type_de_champ_id"
   end
 
@@ -539,6 +543,8 @@ ActiveRecord::Schema.define(version: 2018_12_18_204707) do
     t.datetime "updated_at"
     t.jsonb "options"
     t.bigint "stable_id"
+    t.bigint "parent_id"
+    t.index ["parent_id"], name: "index_types_de_champ_on_parent_id"
     t.index ["private"], name: "index_types_de_champ_on_private"
     t.index ["stable_id"], name: "index_types_de_champ_on_stable_id"
   end
@@ -600,6 +606,7 @@ ActiveRecord::Schema.define(version: 2018_12_18_204707) do
   add_foreign_key "attestation_templates", "procedures"
   add_foreign_key "attestations", "dossiers"
   add_foreign_key "avis", "gestionnaires", column: "claimant_id"
+  add_foreign_key "champs", "champs", column: "parent_id"
   add_foreign_key "closed_mails", "procedures"
   add_foreign_key "commentaires", "dossiers"
   add_foreign_key "dossier_operation_logs", "dossiers"
@@ -613,5 +620,6 @@ ActiveRecord::Schema.define(version: 2018_12_18_204707) do
   add_foreign_key "received_mails", "procedures"
   add_foreign_key "refused_mails", "procedures"
   add_foreign_key "services", "administrateurs"
+  add_foreign_key "types_de_champ", "types_de_champ", column: "parent_id"
   add_foreign_key "without_continuation_mails", "procedures"
 end
diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb
index f9c345837..20d04628d 100644
--- a/spec/factories/champ.rb
+++ b/spec/factories/champ.rb
@@ -165,4 +165,8 @@ FactoryBot.define do
       champ.etablissement.signature = champ.etablissement.sign
     end
   end
+
+  factory :champ_repetition, class: 'Champs::RepetitionChamp' do
+    type_de_champ { create(:type_de_champ_repetition) }
+  end
 end
diff --git a/spec/factories/type_de_champ.rb b/spec/factories/type_de_champ.rb
index caf15f043..26df602b2 100644
--- a/spec/factories/type_de_champ.rb
+++ b/spec/factories/type_de_champ.rb
@@ -93,6 +93,9 @@ FactoryBot.define do
     factory :type_de_champ_carte do
       type_champ { TypeDeChamp.type_champs.fetch(:carte) }
     end
+    factory :type_de_champ_repetition do
+      type_champ { TypeDeChamp.type_champs.fetch(:repetition) }
+    end
 
     trait :private do
       private { true }
diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb
index 9056d7d40..a47d82206 100644
--- a/spec/models/champ_spec.rb
+++ b/spec/models/champ_spec.rb
@@ -399,4 +399,30 @@ describe Champ do
       it { expect{ champ.save }.to_not change(VirusScan, :count) }
     end
   end
+
+  describe "repetition" do
+    let(:champ) { create(:champ_repetition) }
+    let(:champ_text) { create(:champ_text, row: 0) }
+    let(:champ_integer_number) { create(:champ_integer_number, row: 0) }
+    let(:champ_text2) { create(:champ_text, row: 1) }
+
+    it {
+      expect(champ.rows.size).to eq(0)
+
+      champ.champs << champ_text2
+      expect(champ.rows.size).to eq(1)
+
+      champ.champs << champ_integer_number
+      row = champ.reload.rows.first
+      expect(row.size).to eq(1)
+      expect(row.first).to eq(champ_integer_number)
+
+      champ.champs << champ_text
+      row = champ.reload.rows.first
+      expect(row.size).to eq(2)
+      expect(row.second).to eq(champ_text)
+
+      expect(champ.rows.size).to eq(2)
+    }
+  end
 end
diff --git a/spec/models/type_de_champ_shared_example.rb b/spec/models/type_de_champ_shared_example.rb
index 5fbceb3bd..a1cde8dfd 100644
--- a/spec/models/type_de_champ_shared_example.rb
+++ b/spec/models/type_de_champ_shared_example.rb
@@ -110,4 +110,20 @@ shared_examples 'type_de_champ_spec' do
       end
     end
   end
+
+  describe "repetition" do
+    let(:type_de_champ) { create(:type_de_champ_repetition) }
+    let(:type_de_champ_text) { create(:type_de_champ_text) }
+    let(:type_de_champ_integer_number) { create(:type_de_champ_integer_number) }
+
+    it {
+      expect(type_de_champ.types_de_champ.size).to eq(0)
+      type_de_champ.types_de_champ << type_de_champ_integer_number
+      expect(type_de_champ.types_de_champ.size).to eq(1)
+      type_de_champ.types_de_champ << type_de_champ_text
+      expect(type_de_champ.types_de_champ.size).to eq(2)
+      expect(type_de_champ_integer_number.parent).to eq(type_de_champ)
+      expect(type_de_champ_text.parent).to eq(type_de_champ)
+    }
+  end
 end
diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb
index 67e50a4b7..5dcad08be 100644
--- a/spec/services/procedure_export_service_spec.rb
+++ b/spec/services/procedure_export_service_spec.rb
@@ -55,8 +55,6 @@ describe ProcedureExportService do
           :regions,
           :departements,
           :engagement,
-          :header_section,
-          :explication,
           :dossier_link,
           :piece_justificative,
           :siret,
@@ -123,7 +121,7 @@ describe ProcedureExportService do
         }
 
         let(:champs_data) {
-          dossier.champs.ordered.map(&:for_export)
+          dossier.reload.champs.reject(&:exclude_from_export?).map(&:for_export)
         }
 
         let(:etablissement_data) {
@@ -132,8 +130,8 @@ describe ProcedureExportService do
 
         it 'should have values' do
           expect(data.first[0..14]).to eq(dossier_data)
-          expect(data.first[15..40]).to eq(champs_data)
-          expect(data.first[41..64]).to eq(etablissement_data)
+          expect(data.first[15..38]).to eq(champs_data)
+          expect(data.first[39..62]).to eq(etablissement_data)
 
           expect(data).to eq([
             dossier_data + champs_data + etablissement_data
@@ -178,8 +176,8 @@ describe ProcedureExportService do
 
           it 'should have values' do
             expect(data.first[0..14]).to eq(dossier_data)
-            expect(data.first[15..40]).to eq(champs_data)
-            expect(data.first[41..64]).to eq(etablissement_data)
+            expect(data.first[15..38]).to eq(champs_data)
+            expect(data.first[39..62]).to eq(etablissement_data)
 
             expect(data).to eq([
               dossier_data + champs_data + etablissement_data