diff --git a/composer.json b/composer.json
index d1ee000c4b3d0b4c45ca376cc5e1a483f0f74932..4ffa600e20fb4070a37d2680a7574969e14e313d 100644
--- a/composer.json
+++ b/composer.json
@@ -10,8 +10,7 @@
   "require": {
     "drupal/autocomplete_id": "^1.5",
     "drupal/charts": "^5.0",
-    "drupal/core": "^9 || ^10 || ^11",
-    "drupal/field_group": "^3.4",
+    "drupal/core": "^10 || ^11",
     "drupal/moderation_note": "^1.0-beta5",
     "drupal/search_api": "^1.0",
     "drupal/token": "^1.13",
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_correct_template.yml b/config/install/field.field.knowledge_competency.knowledge_competency.field_correct_template.yml
deleted file mode 100644
index bf42be990ca2452f1c4d61fda79e9cfa30704018..0000000000000000000000000000000000000000
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_correct_template.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.knowledge_competency.field_correct_template
-  module:
-    - knowledge
-id: knowledge_competency.knowledge_competency.field_correct_template
-field_name: field_correct_template
-entity_type: knowledge_competency
-bundle: knowledge_competency
-label: 'Correct Template'
-description: 'Understands Solve Loop templates and, given a request, can select the correct one'
-required: false
-translatable: false
-default_value:
-  -
-    value: 0
-default_value_callback: ''
-settings:
-  on_label: 'Yes'
-  off_label: 'No'
-field_type: boolean
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_note.yml b/config/install/field.field.knowledge_competency.knowledge_competency.field_note.yml
deleted file mode 100644
index d422e35cfa73320ef98e7b3d852e64834597e6c4..0000000000000000000000000000000000000000
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_note.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.knowledge_competency.field_note
-  module:
-    - knowledge
-    - text
-id: knowledge_competency.knowledge_competency.field_note
-field_name: field_note
-entity_type: knowledge_competency
-bundle: knowledge_competency
-label: Note
-description: ''
-required: false
-translatable: false
-default_value: {  }
-default_value_callback: ''
-settings: {  }
-field_type: text_long
diff --git a/config/install/field.field.user.user.knowledge_coach.yml b/config/install/field.field.user.user.knowledge_coach.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bbdebbe30fed55730f1c4ac856a28eb6c78a3f34
--- /dev/null
+++ b/config/install/field.field.user.user.knowledge_coach.yml
@@ -0,0 +1,29 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.user.knowledge_coach
+  module:
+    - user
+id: user.user.knowledge_coach
+field_name: knowledge_coach
+entity_type: user
+bundle: user
+label: Coach
+description: ''
+required: false
+translatable: false
+default_value: {  }
+default_value_callback: ''
+settings:
+  handler: 'default:user'
+  handler_settings:
+    target_bundles: null
+    sort:
+      field: _none
+      direction: ASC
+    auto_create: false
+    filter:
+      type: _none
+    include_anonymous: false
+field_type: entity_reference
diff --git a/config/install/field.field.user.user.knowledge_leader.yml b/config/install/field.field.user.user.knowledge_leader.yml
new file mode 100644
index 0000000000000000000000000000000000000000..561a5dde5d1f901b85efa1f3e62ab1746ae7e8d3
--- /dev/null
+++ b/config/install/field.field.user.user.knowledge_leader.yml
@@ -0,0 +1,29 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.user.knowledge_leader
+  module:
+    - user
+id: user.user.knowledge_leader
+field_name: knowledge_leader
+entity_type: user
+bundle: user
+label: Leader
+description: ''
+required: false
+translatable: false
+default_value: {  }
+default_value_callback: ''
+settings:
+  handler: 'default:user'
+  handler_settings:
+    target_bundles: null
+    sort:
+      field: _none
+      direction: ASC
+    auto_create: false
+    filter:
+      type: _none
+    include_anonymous: false
+field_type: entity_reference
diff --git a/config/install/field.storage.knowledge_competency.field_correct_template.yml b/config/install/field.storage.knowledge_competency.field_correct_template.yml
deleted file mode 100644
index 4a4429a2bc8fa8f70a4c6a69683a4a1ee4716399..0000000000000000000000000000000000000000
--- a/config/install/field.storage.knowledge_competency.field_correct_template.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - knowledge
-id: knowledge_competency.field_correct_template
-field_name: field_correct_template
-entity_type: knowledge_competency
-type: boolean
-settings: {  }
-module: core
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/field.storage.knowledge_competency.field_note.yml b/config/install/field.storage.knowledge_competency.field_note.yml
deleted file mode 100644
index a4136c886e531021f9bc46e8550087358ebbb530..0000000000000000000000000000000000000000
--- a/config/install/field.storage.knowledge_competency.field_note.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - knowledge
-    - text
-id: knowledge_competency.field_note
-field_name: field_note
-entity_type: knowledge_competency
-type: text_long
-settings: {  }
-module: text
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/knowledge.competency.settings.yml b/config/install/knowledge.competency.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..239cc478bfdf9013535e69d05211a33281218e3a
--- /dev/null
+++ b/config/install/knowledge.competency.settings.yml
@@ -0,0 +1,16 @@
+roles:
+  -
+    role: knowledge_candidate
+    weight: 0
+    action: auto
+    promote: self
+  -
+    role: knowledge_contributor
+    weight: 1
+    action: auto
+    promote: self
+  -
+    role: knowledge_publisher
+    weight: 2
+    action: auto
+    promote: self
diff --git a/config/install/user.role.knowledge_leader.yml b/config/install/user.role.knowledge_leader.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b6e8fa8e415574a5c97dcc596687b5e29e6c4855
--- /dev/null
+++ b/config/install/user.role.knowledge_leader.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - knowledge
+id: knowledge_leader
+label: Leader
+weight: 7
+is_admin: null
+permissions: {}
diff --git a/config/optional/core.entity_form_display.knowledge_competency.knowledge_competency.default.yml b/config/optional/core.entity_form_display.knowledge_competency.knowledge_competency.default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..714b35c3b77d3d5daea0802b3a163df7be918723
--- /dev/null
+++ b/config/optional/core.entity_form_display.knowledge_competency.knowledge_competency.default.yml
@@ -0,0 +1,212 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.knowledge_competency.knowledge_competency.field_audience
+    - field.field.knowledge_competency.knowledge_competency.field_business
+    - field.field.knowledge_competency.knowledge_competency.field_capture_context
+    - field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment
+    - field.field.knowledge_competency.knowledge_competency.field_collaborate
+    - field.field.knowledge_competency.knowledge_competency.field_complete_thoughts
+    - field.field.knowledge_competency.knowledge_competency.field_confidence
+    - field.field.knowledge_competency.knowledge_competency.field_content_standard
+    - field.field.knowledge_competency.knowledge_competency.field_documents_request
+    - field.field.knowledge_competency.knowledge_competency.field_fix_it
+    - field.field.knowledge_competency.knowledge_competency.field_flag_it
+    - field.field.knowledge_competency.knowledge_competency.field_improve
+    - field.field.knowledge_competency.knowledge_competency.field_includes_context
+    - field.field.knowledge_competency.knowledge_competency.field_iterative_search
+    - field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements
+    - field.field.knowledge_competency.knowledge_competency.field_link_it
+    - field.field.knowledge_competency.knowledge_competency.field_one
+    - field.field.knowledge_competency.knowledge_competency.field_process_adherence
+    - field.field.knowledge_competency.knowledge_competency.field_relevant
+    - field.field.knowledge_competency.knowledge_competency.field_reuse
+    - field.field.knowledge_competency.knowledge_competency.field_search_it
+    - field.field.knowledge_competency.knowledge_competency.field_solve_loop
+    - field.field.knowledge_competency.knowledge_competency.field_structure
+    - field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve
+    - field.field.knowledge_competency.knowledge_competency.field_update_or_create
+  module:
+    - knowledge
+id: knowledge_competency.knowledge_competency.default
+targetEntityType: knowledge_competency
+bundle: knowledge_competency
+mode: default
+content:
+  field_audience:
+    type: boolean_checkbox
+    weight: 15
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_business:
+    type: boolean_checkbox
+    weight: 3
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_capture_context:
+    type: boolean_checkbox
+    weight: 14
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_capture_in_the_moment:
+    type: boolean_checkbox
+    weight: 23
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_collaborate:
+    type: boolean_checkbox
+    weight: 24
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_complete_thoughts:
+    type: boolean_checkbox
+    weight: 9
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_confidence:
+    type: boolean_checkbox
+    weight: 22
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_content_standard:
+    type: boolean_checkbox
+    weight: 16
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_documents_request:
+    type: boolean_checkbox
+    weight: 4
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_fix_it:
+    type: boolean_checkbox
+    weight: 5
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_flag_it:
+    type: boolean_checkbox
+    weight: 2
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_improve:
+    type: boolean_checkbox
+    weight: 20
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_includes_context:
+    type: boolean_checkbox
+    weight: 11
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_iterative_search:
+    type: boolean_checkbox
+    weight: 19
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_kcs_article_elements:
+    type: boolean_checkbox
+    weight: 7
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_link_it:
+    type: boolean_checkbox
+    weight: 1
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_one:
+    type: boolean_checkbox
+    weight: 10
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_process_adherence:
+    type: boolean_checkbox
+    weight: 21
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_relevant:
+    type: boolean_checkbox
+    weight: 18
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_reuse:
+    type: boolean_checkbox
+    weight: 6
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_search_it:
+    type: boolean_checkbox
+    weight: 0
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_solve_loop:
+    type: boolean_checkbox
+    weight: 13
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_structure:
+    type: boolean_checkbox
+    weight: 8
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_sufficient_to_solve:
+    type: boolean_checkbox
+    weight: 17
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+  field_update_or_create:
+    type: boolean_checkbox
+    weight: 12
+    region: content
+    settings:
+      display_label: true
+    third_party_settings: {  }
+hidden: {  }
diff --git a/config/optional/core.entity_view_display.knowledge_competency.knowledge_competency.default.yml b/config/optional/core.entity_view_display.knowledge_competency.knowledge_competency.default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ab395f916809334a918fcddd97dfb94ca6b82b6
--- /dev/null
+++ b/config/optional/core.entity_view_display.knowledge_competency.knowledge_competency.default.yml
@@ -0,0 +1,298 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.knowledge_competency.knowledge_competency.field_audience
+    - field.field.knowledge_competency.knowledge_competency.field_business
+    - field.field.knowledge_competency.knowledge_competency.field_capture_context
+    - field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment
+    - field.field.knowledge_competency.knowledge_competency.field_collaborate
+    - field.field.knowledge_competency.knowledge_competency.field_complete_thoughts
+    - field.field.knowledge_competency.knowledge_competency.field_confidence
+    - field.field.knowledge_competency.knowledge_competency.field_content_standard
+    - field.field.knowledge_competency.knowledge_competency.field_documents_request
+    - field.field.knowledge_competency.knowledge_competency.field_fix_it
+    - field.field.knowledge_competency.knowledge_competency.field_flag_it
+    - field.field.knowledge_competency.knowledge_competency.field_improve
+    - field.field.knowledge_competency.knowledge_competency.field_includes_context
+    - field.field.knowledge_competency.knowledge_competency.field_iterative_search
+    - field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements
+    - field.field.knowledge_competency.knowledge_competency.field_link_it
+    - field.field.knowledge_competency.knowledge_competency.field_one
+    - field.field.knowledge_competency.knowledge_competency.field_process_adherence
+    - field.field.knowledge_competency.knowledge_competency.field_relevant
+    - field.field.knowledge_competency.knowledge_competency.field_reuse
+    - field.field.knowledge_competency.knowledge_competency.field_search_it
+    - field.field.knowledge_competency.knowledge_competency.field_solve_loop
+    - field.field.knowledge_competency.knowledge_competency.field_structure
+    - field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve
+    - field.field.knowledge_competency.knowledge_competency.field_update_or_create
+  module:
+    - knowledge
+id: knowledge_competency.knowledge_competency.default
+targetEntityType: knowledge_competency
+bundle: knowledge_competency
+mode: default
+content:
+  field_audience:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 15
+    region: content
+  field_business:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 3
+    region: content
+  field_capture_context:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 14
+    region: content
+  field_capture_in_the_moment:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 23
+    region: content
+  field_collaborate:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 24
+    region: content
+  field_complete_thoughts:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 9
+    region: content
+  field_confidence:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 22
+    region: content
+  field_content_standard:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 16
+    region: content
+  field_documents_request:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 4
+    region: content
+  field_fix_it:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 5
+    region: content
+  field_flag_it:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 2
+    region: content
+  field_improve:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 20
+    region: content
+  field_includes_context:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 11
+    region: content
+  field_iterative_search:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 19
+    region: content
+  field_kcs_article_elements:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 7
+    region: content
+  field_link_it:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 1
+    region: content
+  field_one:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 10
+    region: content
+  field_process_adherence:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 21
+    region: content
+  field_relevant:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 18
+    region: content
+  field_reuse:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 6
+    region: content
+  field_search_it:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 0
+    region: content
+  field_solve_loop:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 13
+    region: content
+  field_structure:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 8
+    region: content
+  field_sufficient_to_solve:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 17
+    region: content
+  field_update_or_create:
+    type: knowledge_competency
+    label: inline
+    settings:
+      format: default
+      format_custom_false: ''
+      format_custom_true: ''
+    third_party_settings: {  }
+    weight: 12
+    region: content
+hidden:
+  contributor_approved: true
+  contributor_coach: true
+  contributor_leader: true
+  contributor_proposed: true
+  publisher_approved: true
+  publisher_coach: true
+  publisher_leader: true
+  publisher_proposed: true
+  roles: true
+  search_api_excerpt: true
+  user_id: true
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_audience.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_audience.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_audience.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_audience.yml
index ce1496876deeda35976e5f5076ad009cb4c5966e..385a76e31d57c2d987d5cd398fdc7ba4baf87f7f 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_audience.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_audience.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_audience
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_audience
 field_name: field_audience
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_business.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_business.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_business.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_business.yml
index 80871a79bf8d663aedef28ef532872ea34c5d74a..9b9194dce83c0543e33c96793396236fd0760907 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_business.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_business.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_business
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_business
 field_name: field_business
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml
similarity index 74%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml
index 767b9599b3137a213ce3a0aaa44cc6d7732e48f9..f6e8f148c51170332fc4a3cf0f99b7a6418f62e3 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_context.yml
@@ -5,12 +5,15 @@ dependencies:
     - field.storage.knowledge_competency.field_capture_context
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_capture_context
 field_name: field_capture_context
 entity_type: knowledge_competency
 bundle: knowledge_competency
 label: 'Capture Context'
-description: 'Consistently demonstrates the ability to capture the requestor''s context'
+description: "Consistently demonstrates the ability to capture the requestor's context"
 required: false
 translatable: false
 default_value:
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml
similarity index 90%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml
index 730f6c5424c59e3f92ee4f13a2a83da1554f7b26..0e6dca6916bdc1f30824ceb32f6abb191a16c000 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_capture_in_the_moment.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_capture_in_the_moment
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_capture_in_the_moment
 field_name: field_capture_in_the_moment
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml
index cdf1d9a67d8fc5ea042f94711055f3b82c6eb4a0..882a9725834e685e532a0bbcabbdf8075e63038d 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_collaborate.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_collaborate
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_collaborate
 field_name: field_collaborate
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml
index f98cd3692c44e14e2c1367f09f7f164c9e501ffa..9c1f5fcd60c7f97ec4ab42e76b7df7ac11b2f413 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_complete_thoughts.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_complete_thoughts
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_complete_thoughts
 field_name: field_complete_thoughts
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_confidence.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_confidence.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_confidence.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_confidence.yml
index a7b5a5b693840430ca60d4ad929a92a8738ea64d..4ce7751826dcaddc219e8e2634c01373f227ae7f 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_confidence.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_confidence.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_confidence
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_confidence
 field_name: field_confidence
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml
similarity index 89%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml
index 3cc18d5de132f0e22ca68e10b817df2d313e8df2..1a53a01a5269ebc661b80ba07c0486a2f84602d3 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_content_standard.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_content_standard
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_content_standard
 field_name: field_content_standard
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml
index 1dc1c68e96fca0e74cc73c3aa7f0d0ee41503132..f197fbfa6f0b1f653d51592eedc41b11eff510b3 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_documents_request.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_documents_request
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_documents_request
 field_name: field_documents_request
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml
index e6bfbd9f495926ae90b190a936e8618381b72c6b..3d8b2e85c6cb1e2226ef38f4ba6481bd8d74065c 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_fix_it.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_fix_it
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_fix_it
 field_name: field_fix_it
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml
index 0552f5519562cc8b565d740d066ff200ac536fd3..d7aa78cb5edb9150f84bda97a09dcb9c603c7503 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_flag_it.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_flag_it
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_candidate
 id: knowledge_competency.knowledge_competency.field_flag_it
 field_name: field_flag_it
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_improve.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_improve.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_improve.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_improve.yml
index 27348da89a9a218c43d03f9c4ecc1a45df728cee..be3c6d9cf40ffd0db7ef31f6429919fce282a30e 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_improve.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_improve.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_improve
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_improve
 field_name: field_improve
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml
similarity index 79%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml
index 7f89c567676ecaa56897aa2a4bd4773635e437e4..4be4fb30d7588f251a597147d4da18fe5a943089 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_includes_context.yml
@@ -5,12 +5,15 @@ dependencies:
     - field.storage.knowledge_competency.field_includes_context
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_includes_context
 field_name: field_includes_context
 entity_type: knowledge_competency
 bundle: knowledge_competency
 label: 'Includes Context'
-description: 'Includes the requestor''s context'
+description: "Includes the requestor's context"
 required: false
 translatable: false
 default_value:
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml
similarity index 69%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml
index c5ea7db364e6df2efa07e17f13ecad4017cc0a16..103e970cccf39e6225e49d1585036203d92cc86c 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_iterative_search.yml
@@ -5,12 +5,15 @@ dependencies:
     - field.storage.knowledge_competency.field_iterative_search
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_iterative_search
 field_name: field_iterative_search
 entity_type: knowledge_competency
 bundle: knowledge_competency
 label: 'Iterative Search'
-description: 'Effectively and iteratively searches using the requestor''s context and additional information gathered during the interaction'
+description: "Effectively and iteratively searches using the requestor's context and additional information gathered during the interaction"
 required: false
 translatable: false
 default_value:
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml
index 4738b7815a37c06b65e9c3f363ae26e5e8ae7333..8f1e258029a46fae4e472d504c677ad9465cdb80 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_kcs_article_elements.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_kcs_article_elements
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_kcs_article_elements
 field_name: field_kcs_article_elements
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_link_it.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_link_it.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_link_it.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_link_it.yml
index e5a917ec99b85b132251a190ccbb56bb05be83cf..f8ac4611dd9a8e19603823a9cd2e0115f89ed0d3 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_link_it.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_link_it.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_link_it
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_candidate
 id: knowledge_competency.knowledge_competency.field_link_it
 field_name: field_link_it
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_one.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_one.yml
similarity index 85%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_one.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_one.yml
index 83e602b75ad870012339993d3505a96d60172271..0ffd14e29044f054e7127f8a84ad838d2fe256b9 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_one.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_one.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_one
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_one
 field_name: field_one
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml
similarity index 89%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml
index 6562e48fcf6b79335272102c4a9c700682c1f134..58badd8ff5e160c20c721de26e05def1a0e51d76 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_process_adherence.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_process_adherence
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_process_adherence
 field_name: field_process_adherence
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_relevant.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_relevant.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_relevant.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_relevant.yml
index f4298a956b7c0cfe86f75567ceadf3cafc538342..b0475da7308769d4b81cdb965863ab2c22b3863b 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_relevant.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_relevant.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_relevant
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_relevant
 field_name: field_relevant
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_reuse.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_reuse.yml
similarity index 87%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_reuse.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_reuse.yml
index 8599a8d79e7680d798568eb4b646891aa9d04e61..4bd3bffc9f68b6225bcb3483e0de1c323f03c3db 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_reuse.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_reuse.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_reuse
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_reuse
 field_name: field_reuse
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_search_it.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_search_it.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_search_it.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_search_it.yml
index eb6d2049955e3841cc5de2c7f92a7363f88b64cf..c0616a686e4d866cb3bdd88383be181e2c5c2c5f 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_search_it.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_search_it.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_search_it
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_candidate
 id: knowledge_competency.knowledge_competency.field_search_it
 field_name: field_search_it
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml
similarity index 88%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml
index 2ed6f537b68b7e544376e1d04fc39ad1a0231f5f..9e34fa52c29508832c82b849e82c5ee91665eba9 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_solve_loop.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_solve_loop
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_solve_loop
 field_name: field_solve_loop
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_structure.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_structure.yml
similarity index 86%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_structure.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_structure.yml
index f99548533725aeb994c4ad0df45f874f8621bd13..5a1d18443f9f099d4bcba68acbec10560fe76a4a 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_structure.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_structure.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_structure
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_structure
 field_name: field_structure
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml
similarity index 89%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml
index c36b7648d96e0b7b0d06119eda02ec7fb859034b..eb78bf4d4610280ffad692dc8960807fd80e4a21 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_sufficient_to_solve.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_sufficient_to_solve
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_publisher
 id: knowledge_competency.knowledge_competency.field_sufficient_to_solve
 field_name: field_sufficient_to_solve
 entity_type: knowledge_competency
diff --git a/config/install/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml b/config/optional/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml
similarity index 89%
rename from config/install/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml
rename to config/optional/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml
index f20bce6a75a2a86eb5f488d451d7beecbe0d5a6c..ae789c3fb238499bd9ee0d2614c2cf91839c0805 100644
--- a/config/install/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml
+++ b/config/optional/field.field.knowledge_competency.knowledge_competency.field_update_or_create.yml
@@ -5,6 +5,9 @@ dependencies:
     - field.storage.knowledge_competency.field_update_or_create
   module:
     - knowledge
+third_party_settings:
+  knowledge:
+    competency_role: knowledge_contributor
 id: knowledge_competency.knowledge_competency.field_update_or_create
 field_name: field_update_or_create
 entity_type: knowledge_competency
diff --git a/config/install/field.storage.knowledge_competency.field_audience.yml b/config/optional/field.storage.knowledge_competency.field_audience.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_audience.yml
rename to config/optional/field.storage.knowledge_competency.field_audience.yml
diff --git a/config/install/field.storage.knowledge_competency.field_business.yml b/config/optional/field.storage.knowledge_competency.field_business.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_business.yml
rename to config/optional/field.storage.knowledge_competency.field_business.yml
diff --git a/config/install/field.storage.knowledge_competency.field_capture_context.yml b/config/optional/field.storage.knowledge_competency.field_capture_context.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_capture_context.yml
rename to config/optional/field.storage.knowledge_competency.field_capture_context.yml
diff --git a/config/install/field.storage.knowledge_competency.field_capture_in_the_moment.yml b/config/optional/field.storage.knowledge_competency.field_capture_in_the_moment.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_capture_in_the_moment.yml
rename to config/optional/field.storage.knowledge_competency.field_capture_in_the_moment.yml
diff --git a/config/install/field.storage.knowledge_competency.field_collaborate.yml b/config/optional/field.storage.knowledge_competency.field_collaborate.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_collaborate.yml
rename to config/optional/field.storage.knowledge_competency.field_collaborate.yml
diff --git a/config/install/field.storage.knowledge_competency.field_complete_thoughts.yml b/config/optional/field.storage.knowledge_competency.field_complete_thoughts.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_complete_thoughts.yml
rename to config/optional/field.storage.knowledge_competency.field_complete_thoughts.yml
diff --git a/config/install/field.storage.knowledge_competency.field_confidence.yml b/config/optional/field.storage.knowledge_competency.field_confidence.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_confidence.yml
rename to config/optional/field.storage.knowledge_competency.field_confidence.yml
diff --git a/config/install/field.storage.knowledge_competency.field_content_standard.yml b/config/optional/field.storage.knowledge_competency.field_content_standard.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_content_standard.yml
rename to config/optional/field.storage.knowledge_competency.field_content_standard.yml
diff --git a/config/install/field.storage.knowledge_competency.field_documents_request.yml b/config/optional/field.storage.knowledge_competency.field_documents_request.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_documents_request.yml
rename to config/optional/field.storage.knowledge_competency.field_documents_request.yml
diff --git a/config/install/field.storage.knowledge_competency.field_fix_it.yml b/config/optional/field.storage.knowledge_competency.field_fix_it.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_fix_it.yml
rename to config/optional/field.storage.knowledge_competency.field_fix_it.yml
diff --git a/config/install/field.storage.knowledge_competency.field_flag_it.yml b/config/optional/field.storage.knowledge_competency.field_flag_it.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_flag_it.yml
rename to config/optional/field.storage.knowledge_competency.field_flag_it.yml
diff --git a/config/install/field.storage.knowledge_competency.field_improve.yml b/config/optional/field.storage.knowledge_competency.field_improve.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_improve.yml
rename to config/optional/field.storage.knowledge_competency.field_improve.yml
diff --git a/config/install/field.storage.knowledge_competency.field_includes_context.yml b/config/optional/field.storage.knowledge_competency.field_includes_context.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_includes_context.yml
rename to config/optional/field.storage.knowledge_competency.field_includes_context.yml
diff --git a/config/install/field.storage.knowledge_competency.field_iterative_search.yml b/config/optional/field.storage.knowledge_competency.field_iterative_search.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_iterative_search.yml
rename to config/optional/field.storage.knowledge_competency.field_iterative_search.yml
diff --git a/config/install/field.storage.knowledge_competency.field_kcs_article_elements.yml b/config/optional/field.storage.knowledge_competency.field_kcs_article_elements.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_kcs_article_elements.yml
rename to config/optional/field.storage.knowledge_competency.field_kcs_article_elements.yml
diff --git a/config/install/field.storage.knowledge_competency.field_link_it.yml b/config/optional/field.storage.knowledge_competency.field_link_it.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_link_it.yml
rename to config/optional/field.storage.knowledge_competency.field_link_it.yml
diff --git a/config/install/field.storage.knowledge_competency.field_one.yml b/config/optional/field.storage.knowledge_competency.field_one.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_one.yml
rename to config/optional/field.storage.knowledge_competency.field_one.yml
diff --git a/config/install/field.storage.knowledge_competency.field_process_adherence.yml b/config/optional/field.storage.knowledge_competency.field_process_adherence.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_process_adherence.yml
rename to config/optional/field.storage.knowledge_competency.field_process_adherence.yml
diff --git a/config/install/field.storage.knowledge_competency.field_relevant.yml b/config/optional/field.storage.knowledge_competency.field_relevant.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_relevant.yml
rename to config/optional/field.storage.knowledge_competency.field_relevant.yml
diff --git a/config/install/field.storage.knowledge_competency.field_reuse.yml b/config/optional/field.storage.knowledge_competency.field_reuse.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_reuse.yml
rename to config/optional/field.storage.knowledge_competency.field_reuse.yml
diff --git a/config/install/field.storage.knowledge_competency.field_search_it.yml b/config/optional/field.storage.knowledge_competency.field_search_it.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_search_it.yml
rename to config/optional/field.storage.knowledge_competency.field_search_it.yml
diff --git a/config/install/field.storage.knowledge_competency.field_solve_loop.yml b/config/optional/field.storage.knowledge_competency.field_solve_loop.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_solve_loop.yml
rename to config/optional/field.storage.knowledge_competency.field_solve_loop.yml
diff --git a/config/install/field.storage.knowledge_competency.field_structure.yml b/config/optional/field.storage.knowledge_competency.field_structure.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_structure.yml
rename to config/optional/field.storage.knowledge_competency.field_structure.yml
diff --git a/config/install/field.storage.knowledge_competency.field_sufficient_to_solve.yml b/config/optional/field.storage.knowledge_competency.field_sufficient_to_solve.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_sufficient_to_solve.yml
rename to config/optional/field.storage.knowledge_competency.field_sufficient_to_solve.yml
diff --git a/config/install/field.storage.knowledge_competency.field_update_or_create.yml b/config/optional/field.storage.knowledge_competency.field_update_or_create.yml
similarity index 100%
rename from config/install/field.storage.knowledge_competency.field_update_or_create.yml
rename to config/optional/field.storage.knowledge_competency.field_update_or_create.yml
diff --git a/config/install/field.storage.node.knowledge.yml b/config/optional/field.storage.node.knowledge.yml
similarity index 100%
rename from config/install/field.storage.node.knowledge.yml
rename to config/optional/field.storage.node.knowledge.yml
diff --git a/config/install/field.storage.node.knowledge_audience.yml b/config/optional/field.storage.node.knowledge_audience.yml
similarity index 100%
rename from config/install/field.storage.node.knowledge_audience.yml
rename to config/optional/field.storage.node.knowledge_audience.yml
diff --git a/config/install/field.storage.node.knowledge_governance.yml b/config/optional/field.storage.node.knowledge_governance.yml
similarity index 100%
rename from config/install/field.storage.node.knowledge_governance.yml
rename to config/optional/field.storage.node.knowledge_governance.yml
diff --git a/config/install/field.storage.node.knowledge_governance_user.yml b/config/optional/field.storage.node.knowledge_governance_user.yml
similarity index 100%
rename from config/install/field.storage.node.knowledge_governance_user.yml
rename to config/optional/field.storage.node.knowledge_governance_user.yml
diff --git a/config/install/field.storage.node.knowledge_governance_user_role.yml b/config/optional/field.storage.node.knowledge_governance_user_role.yml
similarity index 100%
rename from config/install/field.storage.node.knowledge_governance_user_role.yml
rename to config/optional/field.storage.node.knowledge_governance_user_role.yml
diff --git a/config/optional/views.view.knowledge_competency_progress.yml b/config/optional/views.view.knowledge_competency_progress.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b5958121e214d570c2aa8ebebcdee727a4dcf06d
--- /dev/null
+++ b/config/optional/views.view.knowledge_competency_progress.yml
@@ -0,0 +1,1945 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - knowledge
+id: knowledge_competency_progress
+label: 'Competency Progress'
+module: views
+description: 'The overall progress'
+tag: ''
+base_table: knowledge_competency
+base_field: id
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      fields:
+        id:
+          id: id
+          table: knowledge_competency
+          field: id
+          relationship: none
+          group_type: count
+          admin_label: ''
+          entity_type: knowledge_competency
+          entity_field: id
+          plugin_id: field
+          label: Total
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_search_it:
+          id: field_search_it
+          table: knowledge_competency__field_search_it
+          field: field_search_it
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Search
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: default
+            format_custom_false: ''
+            format_custom_true: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        field_link_it:
+          id: field_link_it
+          table: knowledge_competency__field_link_it
+          field: field_link_it
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Link
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: default
+            format_custom_false: ''
+            format_custom_true: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        field_flag_it:
+          id: field_flag_it
+          table: knowledge_competency__field_flag_it
+          field: field_flag_it
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Flag
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: default
+            format_custom_false: ''
+            format_custom_true: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        field_business:
+          id: field_business
+          table: knowledge_competency__field_business
+          field: field_business
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Business
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_documents_request:
+          id: field_documents_request
+          table: knowledge_competency__field_documents_request
+          field: field_documents_request
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Documents request'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_fix_it:
+          id: field_fix_it
+          table: knowledge_competency__field_fix_it
+          field: field_fix_it
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Fix
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_reuse:
+          id: field_reuse
+          table: knowledge_competency__field_reuse
+          field: field_reuse
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Reuse
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_correct_template:
+          id: field_correct_template
+          table: knowledge_competency__field_correct_template
+          field: field_correct_template
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Correct Template'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_kcs_article_elements:
+          id: field_kcs_article_elements
+          table: knowledge_competency__field_kcs_article_elements
+          field: field_kcs_article_elements
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Article Elements'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_structure:
+          id: field_structure
+          table: knowledge_competency__field_structure
+          field: field_structure
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Structure
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_complete_thoughts:
+          id: field_complete_thoughts
+          table: knowledge_competency__field_complete_thoughts
+          field: field_complete_thoughts
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Complete thoughts'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_one:
+          id: field_one
+          table: knowledge_competency__field_one
+          field: field_one
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'One thing'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_includes_context:
+          id: field_includes_context
+          table: knowledge_competency__field_includes_context
+          field: field_includes_context
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Includes Context'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_update_or_create:
+          id: field_update_or_create
+          table: knowledge_competency__field_update_or_create
+          field: field_update_or_create
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Update or create'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_solve_loop:
+          id: field_solve_loop
+          table: knowledge_competency__field_solve_loop
+          field: field_solve_loop
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Solve Loop'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_audience:
+          id: field_audience
+          table: knowledge_competency__field_audience
+          field: field_audience
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Audience
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_capture_context:
+          id: field_capture_context
+          table: knowledge_competency__field_capture_context
+          field: field_capture_context
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Capture Context'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_capture_in_the_moment:
+          id: field_capture_in_the_moment
+          table: knowledge_competency__field_capture_in_the_moment
+          field: field_capture_in_the_moment
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Capture in the moment'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_collaborate:
+          id: field_collaborate
+          table: knowledge_competency__field_collaborate
+          field: field_collaborate
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Collaborate
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_confidence:
+          id: field_confidence
+          table: knowledge_competency__field_confidence
+          field: field_confidence
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Confidence
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_content_standard:
+          id: field_content_standard
+          table: knowledge_competency__field_content_standard
+          field: field_content_standard
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Content Standard'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_improve:
+          id: field_improve
+          table: knowledge_competency__field_improve
+          field: field_improve
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Improve
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_iterative_search:
+          id: field_iterative_search
+          table: knowledge_competency__field_iterative_search
+          field: field_iterative_search
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Iterative Search'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_process_adherence:
+          id: field_process_adherence
+          table: knowledge_competency__field_process_adherence
+          field: field_process_adherence
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Process Adherence'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_relevant:
+          id: field_relevant
+          table: knowledge_competency__field_relevant
+          field: field_relevant
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: Relevant
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        field_sufficient_to_solve:
+          id: field_sufficient_to_solve
+          table: knowledge_competency__field_sufficient_to_solve
+          field: field_sufficient_to_solve
+          relationship: none
+          group_type: sum
+          admin_label: ''
+          plugin_id: field
+          label: 'Sufficient to solve'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ''
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+      pager:
+        type: none
+        options:
+          offset: 0
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts: {  }
+      arguments: {  }
+      filters: {  }
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          columns:
+            link_to_latest_version: link_to_latest_version
+          default: '-1'
+          info:
+            link_to_latest_version:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          override: true
+          sticky: false
+          summary: ''
+          empty_table: false
+          caption: ''
+          description: ''
+      row:
+        type: fields
+        options:
+          default_field_elements: true
+          inline: {  }
+          separator: ''
+          hide_empty: false
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships: {  }
+      group_by: true
+      header: {  }
+      footer: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_interface'
+      tags: {  }
diff --git a/config/schema/knowledge.schema.yml b/config/schema/knowledge.schema.yml
index 6dd02d06756e48b5ff30e305bd0d731c8c5567a4..1710f3f3d25f5517751917dd68b7cc899c5f97b1 100644
--- a/config/schema/knowledge.schema.yml
+++ b/config/schema/knowledge.schema.yml
@@ -260,3 +260,47 @@ block.settings.knowledge_block:
     field_name:
       type: string
       label: field_name
+
+knowledge.competency.settings:
+  type: config_object
+  label: 'Knowledge settings'
+  mapping:
+    roles:
+      type: sequence
+      orderby: weight
+      sequence:
+        type: mapping
+        mapping:
+          role:
+            type: string
+            label: 'Role'
+          weight:
+            type: integer
+            label: 'Weight'
+          action:
+            type: string
+            label: 'Action'
+          promote:
+            type: string
+            label: 'Promotion'
+
+field.field.*.*.*.third_party.knowledge:
+  type: mapping
+  label: 'Competency field settings'
+  mapping:
+    competency_role:
+      label: 'The role the competency field belongs to'
+      type: string
+
+field.formatter.settings.knowledge_competency:
+  type: mapping
+  mapping:
+    format:
+      type: string
+      label: 'Output format'
+    format_custom_false:
+      type: label
+      label: 'Custom output for FALSE'
+    format_custom_true:
+      type: label
+      label: 'Custom output for TRUE'
diff --git a/css/competency-form.css b/css/competency-form.css
index 5d2089d1e897f52dbed92130e2359e8724da1568..f83391032fb2ced3b836c215dbb4ded7dfec13db 100644
--- a/css/competency-form.css
+++ b/css/competency-form.css
@@ -1,3 +1,6 @@
-form[data-drupal-selector="knowledge-competency-form"] fieldset {
-  width: 95%;
+div[data-drupal-selector="edit-roles-wrapper"] {
+  display: none;
 }
+span.pull-right {
+  margin-left: 10px;
+}
\ No newline at end of file
diff --git a/css/competency.admin.css b/css/competency.admin.css
new file mode 100644
index 0000000000000000000000000000000000000000..55261b571dfcb407f26ac2c665377087a9e9c94c
--- /dev/null
+++ b/css/competency.admin.css
@@ -0,0 +1,47 @@
+/* Block listing page */
+.role-title__action {
+  display: inline-block;
+  margin-left: 1em; /* LTR */
+}
+[dir="rtl"] .role-title__action {
+  margin-right: 1em;
+  margin-left: 0;
+}
+
+/* Block demo mode */
+.field-role {
+  margin-top: 4px;
+  margin-bottom: 4px;
+  padding: 3px;
+  color: #000;
+  background-color: #ff6;
+}
+a.field-demo-backlink,
+a.field-demo-backlink:link,
+a.field-demo-backlink:visited {
+  position: fixed;
+  z-index: 499;
+  left: 20px; /* LTR */
+  padding: 5px 10px;
+  color: #000;
+  border-radius: 0 0 10px 10px;
+  background-color: #b4d7f0;
+  font-family: "Lucida Grande", Verdana, sans-serif;
+  font-size: small;
+  line-height: 20px;
+}
+a.field-demo-backlink:hover {
+  text-decoration: underline;
+}
+
+/* Configure block form - Block description */
+.field-form .form-item-settings-admin-label label {
+  display: inline;
+}
+.field-form .form-item-settings-admin-label label::after {
+  content: ":";
+}
+.field-disabled:not(:hover) {
+  opacity: 0.675;
+  background: #fcfcfa;
+}
diff --git a/js/competency-form.js b/js/competency-form.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc0eafc379474e450947ec6233ffb28377010564
--- /dev/null
+++ b/js/competency-form.js
@@ -0,0 +1,50 @@
+/**
+ * @file
+ * Defines JavaScript behaviors for the node module.
+ */
+
+(function ($, Drupal) {
+
+  /**
+   * Update the summary of a vertical tab.
+   */
+  function updateSummary(element) {
+    const total = $(element).find('input[type="checkbox"]').length;
+    const correct = $(element).find('input[type="checkbox"]:checked').length;
+    var summary = Drupal.t('@correct of @total', {'@correct': correct, '@total': total});
+
+    if (correct > 0) {
+      const percent = Math.round((correct / total) * 100);
+      summary = Drupal.t('@correct of @total <span class="pull-right">@percent%</span>', {'@correct': correct, '@total': total, '@percent': percent});
+    }
+
+
+    $(element).drupalSetSummary(summary);
+  }
+
+  /**
+   * Behaviors for tabs in the node edit form.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches summary behavior for tabs in the node edit form.
+   */
+  Drupal.behaviors.knowledgeCompetencySummaries = {
+    attach(context) {
+
+      $(context).find('details.vertical-tabs__item').each(function () {
+        updateSummary(this);
+      });
+
+      once('knowledge-competency-summaries', 'details.vertical-tabs__item input[type="checkbox"]', context).forEach(function (element) {
+        $(element).on('change', function () {
+          // Your code to execute when the checkbox changes state
+          const parent =$(element).parents('details.vertical-tabs__item');
+          updateSummary(parent);
+        });
+      });
+
+    },
+  };
+})(jQuery, Drupal);
diff --git a/js/knowledge.competency.admin.js b/js/knowledge.competency.admin.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab521bf6d72962a7b5e762a8919de11a82fd877d
--- /dev/null
+++ b/js/knowledge.competency.admin.js
@@ -0,0 +1,304 @@
+/**
+ * @file
+ * Competenc admin behaviors.
+ */
+
+(function (window, $, Drupal, debounce, once) {
+  /**
+   * Filters the block list by a text input search string.
+   *
+   * The text input will have the selector `input.field-filter-text`.
+   *
+   * The target element to do searching in will be in the selector
+   * `input.field-filter-text[data-element]`
+   *
+   * The text source where the text should be found will have the selector
+   * `.field-filter-text-source`
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior for the block filtering.
+   */
+  Drupal.behaviors.knowledgeCompetencyFilterByText = {
+    attach(context, settings) {
+      const $input = $(once('field-filter-text', 'input.field-filter-text'));
+      const $table = $($input.attr('data-element'));
+      let $filterRows;
+
+      /**
+       * Filters the block list.
+       *
+       * @param {jQuery.Event} e
+       *   The jQuery event for the keyup event that triggered the filter.
+       */
+      function filterBlockList(e) {
+        const query = e.target.value.toLowerCase();
+
+        /**
+         * Shows or hides the block entry based on the query.
+         *
+         * @param {number} index
+         *   The index in the loop, as provided by `jQuery.each`
+         * @param {HTMLElement} label
+         *   The label of the block.
+         */
+        function toggleBlockEntry(index, label) {
+          const $row = $(label).parent().parent();
+          const textMatch = label.textContent.toLowerCase().includes(query);
+          $row.toggle(textMatch);
+        }
+
+        // Filter if the length of the query is at least 2 characters.
+        if (query.length >= 2) {
+          $filterRows.each(toggleBlockEntry);
+          Drupal.announce(
+            Drupal.formatPlural(
+              $table.find('tr:visible').length - 1,
+              '1 block is available in the modified list.',
+              '@count blocks are available in the modified list.',
+            ),
+          );
+        } else {
+          $filterRows.each(function (index) {
+            $(this).parent().parent().show();
+          });
+        }
+      }
+
+      if ($table.length) {
+        $filterRows = $table.find('div.field-filter-text-source');
+        $input.on('keyup', debounce(filterBlockList, 200));
+      }
+    },
+  };
+
+  /**
+   * Highlights the block that was just placed into the block listing.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior for the block placement highlighting.
+   */
+  Drupal.behaviors.knowledgeCompetencyHighlightPlacement = {
+    attach(context, settings) {
+      // Ensure that the block we are attempting to scroll to actually exists.
+      if (settings.blockPlacement && $('.js-field-placed').length) {
+        once(
+          'field-highlight',
+          '[data-drupal-selector="edit-fields"]',
+          context,
+        ).forEach((container) => {
+          const $container = $(container);
+          window.scrollTo({
+            top:
+              $('.js-field-placed').offset().top -
+              $container.offset().top +
+              $container.scrollTop(),
+            behavior: 'smooth',
+          });
+        });
+      }
+    },
+  };
+
+  /**
+   * Move a block in the blocks table between regions via select list.
+   *
+   * This behavior is dependent on the tableDrag behavior, since it uses the
+   * objects initialized in that behavior to update the row.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the tableDrag behavior for blocks in block administration.
+   */
+  Drupal.behaviors.knowledgeCompetencyDrag = {
+    attach(context, settings) {
+      // tableDrag is required and we should be on the blocks admin page.
+      if (
+        typeof Drupal.tableDrag === 'undefined' ||
+        typeof Drupal.tableDrag.knowledge_competency === 'undefined'
+      ) {
+        return;
+      }
+
+      /**
+       * Function to check empty regions and toggle classes based on this.
+       *
+       * @param {jQuery} table
+       *   The jQuery object representing the table to inspect.
+       * @param {Drupal.tableDrag.row} rowObject
+       *   Drupal table drag row dropped.
+       */
+      function checkEmptyRegions(table, rowObject) {
+        'debugger';
+        table.find('tr.role-message').each(function () {
+          const $this = $(this);
+          // If the dragged row is in this region, but above the message row,
+          // swap it down one space.
+          if ($this.prev('tr').get(0) === rowObject.element) {
+            // Prevent a recursion problem when using the keyboard to move rows
+            // up.
+            if (
+              rowObject.method !== 'keyboard' ||
+              rowObject.direction === 'down'
+            ) {
+              rowObject.swap('after', this);
+            }
+          }
+          // This region has become empty.
+          if (
+            $this.next('tr').length === 0 ||
+            !$this.next('tr')[0].matches('.draggable')
+          ) {
+            $this.removeClass('role-populated').addClass('role-empty');
+          }
+          // This region has become populated.
+          else if (this.matches('.role-empty')) {
+            $this.removeClass('role-empty').addClass('role-populated');
+          }
+        });
+      }
+
+      /**
+       * Function to update the last placed row with the correct classes.
+       *
+       * @param {jQuery} table
+       *   The jQuery object representing the table to inspect.
+       * @param {Drupal.tableDrag.row} rowObject
+       *   Drupal table drag row dropped.
+       */
+      function updateLastPlaced(table, rowObject) {
+        // Remove the color-success class from new block if applicable.
+        table.find('.color-success').removeClass('color-success');
+        const $rowObject = $(rowObject);
+        if (!rowObject.element.matches('.drag-previous')) {
+          table.find('.drag-previous').removeClass('drag-previous');
+          $rowObject.addClass('drag-previous');
+        }
+      }
+
+      /**
+       * Update block weights in the given region.
+       *
+       * @param {jQuery} table
+       *   Table with draggable items.
+       * @param {string} region
+       *   Machine name of region containing blocks to update.
+       */
+      function updateBlockWeights(table, region) {
+        console.log(region);
+        // Calculate minimum weight.
+        let weight = -Math.round(table.find('.draggable').length / 2);
+        // Update the block weights.
+        table
+          .find(`.role-${region}-message`)
+          .nextUntil('.role-title')
+          .find('select.field-weight')
+          .each(function () {
+            // Increment the weight before assigning it to prevent using the
+            // absolute minimum available weight. This way we always have an
+            // unused upper and lower bound, which makes manually setting the
+            // weights easier for users who prefer to do it that way.
+            this.value = ++weight;
+          });
+      }
+
+      const table = $('#edit-fields');
+      console.log(table);
+      // Get the blocks tableDrag object.
+      const tableDrag = Drupal.tableDrag.knowledge_competency;
+      // Add a handler for when a row is swapped, update empty regions.
+      tableDrag.row.prototype.onSwap = function (swappedRow) {
+        checkEmptyRegions(table, this);
+        updateLastPlaced(table, this);
+      };
+
+      // Add a handler so when a row is dropped, update fields dropped into
+      // new regions.
+      tableDrag.onDrop = function () {
+        const dragObject = this;
+        const $rowElement = $(dragObject.rowObject.element);
+        // Use "role-message" row instead of "region" row because
+        // "role-{region_name}-message" is less prone to regexp match errors.
+        const regionRow = $rowElement.prevAll('tr.role-message').get(0);
+        const regionName = regionRow.className.replace(
+          /([^ ]+[ ]+)*role-([^ ]+)-message([ ]+[^ ]+)*/,
+          '$2',
+        );
+        const regionField = $rowElement.find('select.field-role-select');
+        // Check whether the newly picked region is available for this block.
+        if (regionField.find(`option[value=${regionName}]`).length === 0) {
+          // If not, alert the user and keep the block in its old region
+          // setting.
+          window.alert(Drupal.t('The block cannot be placed in this region.'));
+          // Simulate that there was a selected element change, so the row is
+          // put back to from where the user tried to drag it.
+          regionField.trigger('change');
+        }
+
+        // Update region and weight fields if the region has been changed.
+        if (!regionField[0].matches(`.field-role-${regionName}`)) {
+          const weightField = $rowElement.find('select.field-weight');
+          const oldRegionName = weightField[0].className.replace(
+            /([^ ]+[ ]+)*field-weight-([^ ]+)([ ]+[^ ]+)*/,
+            '$2',
+          );
+          regionField
+            .removeClass(`field-role-${oldRegionName}`)
+            .addClass(`field-role-${regionName}`);
+          weightField
+            .removeClass(`field-weight-${oldRegionName}`)
+            .addClass(`field-weight-${regionName}`);
+          regionField[0].value = regionName;
+        }
+
+        updateBlockWeights(table, regionName);
+      };
+
+      // Add the behavior to each region select list.
+      $(once('field-role-select', 'select.field-role-select', context)).on(
+        'change',
+        function (event) {
+          // Make our new row and select field.
+          const row = $(this).closest('tr');
+          const select = $(this);
+          // Find the correct region and insert the row as the last in the
+          // region.
+          tableDrag.rowObject = new tableDrag.row(row[0]);
+          const regionMessage = table.find(
+            `.role-${select[0].value}-message`,
+          );
+          const regionItems = regionMessage.nextUntil(
+            '.role-message, .role-title',
+          );
+          if (regionItems.length) {
+            regionItems.last().after(row);
+          }
+          // We found that regionMessage is the last row.
+          else {
+            regionMessage.after(row);
+          }
+          updateBlockWeights(table, select[0].value);
+          // Modify empty regions with added or removed fields.
+          checkEmptyRegions(table, tableDrag.rowObject);
+          // Update last placed block indication.
+          updateLastPlaced(table, tableDrag.rowObject);
+          // Show unsaved changes warning.
+          if (!tableDrag.changed) {
+            $(Drupal.theme('tableDragChangedWarning'))
+              .insertBefore(tableDrag.table)
+              .hide()
+              .fadeIn('slow');
+            tableDrag.changed = true;
+          }
+          // Remove focus from selectbox.
+          select.trigger('blur');
+        },
+      );
+    },
+  };
+
+})(window, jQuery, Drupal, Drupal.debounce, once);
diff --git a/knowledge.info.yml b/knowledge.info.yml
index 9033bf03f0b076952123ea02e855cba1104ea623..d4d2d82ac6a0273868b927f0c21ede44d675eddb 100644
--- a/knowledge.info.yml
+++ b/knowledge.info.yml
@@ -2,15 +2,14 @@ name: Knowledge
 type: module
 description: 'Allows users to link incidents to content.'
 package: Knowledge
-core_version_requirement: ^9.0 || ^10.0 || ^11.0
+core_version_requirement: ^10.0 || ^11.0
+configure: knowledge.admin
 dependencies:
   - autocomplete_id:autocomplete_id
   - drupal:content_moderation
   - drupal:field
-  - drupal:field_layout
   - drupal:options
   - drupal:text
-  - field_group:field_group
   - search_api:search_api
   - token:token
-configure: knowledge.admin
+  - knowledge:knowledge_field
diff --git a/knowledge.install b/knowledge.install
index ae74a2f8f13b8061b23c727bbfbe4ea577650417..d3c013fc6afc979fa8268221efb9c5c12a68aab5 100644
--- a/knowledge.install
+++ b/knowledge.install
@@ -6,6 +6,8 @@
  */
 
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\field\Entity\FieldStorageConfig;
 
 /**
@@ -28,6 +30,40 @@ function knowledge_install() {
   // By default, maintain entity statistics for knowledge.
   // @see \Drupal\knowledge\KnowledgeStatisticsInterface
   \Drupal::state()->set('knowledge.maintain_entity_statistics', TRUE);
+  // Rebuild user entity form display for mobile number field.
+  $storage = \Drupal::entityTypeManager()->getStorage('entity_form_display');
+  /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $user_form_display */
+  $user_form_display = $storage->load('user.user.default');
+  if (!$user_form_display) {
+    $user_form_display = $storage->create([
+      'targetEntityType' => 'user',
+      'bundle' => 'user',
+      'mode' => 'default',
+      'status' => TRUE,
+    ]);
+  }
+  $user_form_display
+    ->setComponent('knowledge_coach', [
+      'type' => 'entity_reference_autocomplete',
+      'weight' => 14,
+      'settings' => [
+        'match_operator' => 'CONTAINS',
+        'size' => '60',
+        'placeholder' => '',
+        'match_limit' => 10,
+      ],
+    ])
+    ->setComponent('knowledge_leader', [
+      'type' => 'entity_reference_autocomplete',
+      'weight' => 14,
+      'settings' => [
+        'match_operator' => 'CONTAINS',
+        'size' => '60',
+        'placeholder' => '',
+        'match_limit' => 10,
+      ],
+    ])
+    ->save();
 }
 
 /**
@@ -232,3 +268,420 @@ function knowledge_schema() {
 
   return $schema;
 }
+
+/**
+ * Competency refactor, removes 'Field Group' dependency.
+ */
+function knowledge_update_8103(&$sandbox) {
+  $fields = [
+    'field_audience' => 'knowledge_publisher',
+    'field_business' => 'knowledge_contributor',
+    'field_capture_context' => 'knowledge_publisher',
+    'field_capture_in_the_moment' => 'knowledge_publisher',
+    'field_collaborate' => 'knowledge_publisher',
+    'field_complete_thoughts' => 'knowledge_contributor',
+    'field_confidence' => 'knowledge_publisher',
+    'field_content_standard' => 'knowledge_publisher',
+    'field_documents_request' => 'knowledge_contributor',
+    'field_fix_it' => 'knowledge_contributor',
+    'field_flag_it' => 'knowledge_candidate',
+    'field_improve' => 'knowledge_publisher',
+    'field_includes_context' => 'knowledge_contributor',
+    'field_iterative_search' => 'knowledge_publisher',
+    'field_kcs_article_elements' => 'knowledge_contributor',
+    'field_link_it' => 'knowledge_candidate',
+    'field_one' => 'knowledge_contributor',
+    'field_process_adherence' => 'knowledge_publisher',
+    'field_relevant' => 'knowledge_publisher',
+    'field_reuse' => 'knowledge_contributor',
+    'field_search_it' => 'knowledge_candidate',
+    'field_solve_loop' => 'knowledge_contributor',
+    'field_structure' => 'knowledge_contributor',
+    'field_sufficient_to_solve' => 'knowledge_publisher',
+    'field_update_or_create' => 'knowledge_contributor',
+  ];
+
+  if (!isset($sandbox['progress'])) {
+    // This must be the first run. Initialize the sandbox.
+    $sandbox['progress'] = 0;
+    $sandbox['max'] = 6;
+  }
+
+  // Create knowledge.competency.settings.
+  if ($sandbox['progress'] == 1) {
+    $settings = \Drupal::service('config.factory')
+      ->getEditable('knowledge.competency.settings');
+    $roles = [
+      [
+        'role' => 'knowledge_candidate',
+        'weight' => 0,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_contributor',
+        'weight' => 1,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_publisher',
+        'weight' => 2,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+    ];
+    $settings->set('roles', $roles);
+    $settings->save();
+
+    // Field definition settings.
+    $fields_definitions = \Drupal::service('entity_field.manager')
+      ->getFieldDefinitions('knowledge_competency', 'knowledge_competency');
+
+    foreach ($fields as $field_id => $role) {
+      $fields_definitions[$field_id]->setThirdPartySetting('knowledge', 'competency_role', $role);
+      $fields_definitions[$field_id]->save();
+    }
+
+  }
+
+  // Install new dependency.
+  if ($sandbox['progress'] == 2) {
+    // Install the.
+    \Drupal::service('module_installer')->install(['knowledge_field'], TRUE);
+    // Clear Cache.
+    drupal_flush_all_caches();
+  }
+
+  // Adds new fields.
+  if ($sandbox['progress'] == 3) {
+    $roles_field_definition = BaseFieldDefinition::create('knowledge_competency_role')
+      ->setLabel(t('Roles'))
+      ->setDescription(t('The roles of the user.'))
+      ->setRevisionable(TRUE)
+      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'knowledge_competency_role',
+        'weight' => 0,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'knowledge_competency_role',
+        'weight' => 5,
+      ])
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayConfigurable('form', TRUE);
+
+    \Drupal::entityDefinitionUpdateManager()
+      ->installFieldStorageDefinition('roles', 'knowledge_competency', 'knowledge_competency', $roles_field_definition);
+
+    $completed = BaseFieldDefinition::create('timestamp')
+      ->setLabel(t('Completed'))
+      ->setDescription(t('The time that correct = total.'))
+      ->setRevisionable(FALSE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    \Drupal::entityDefinitionUpdateManager()
+      ->installFieldStorageDefinition('completed', 'knowledge_competency', 'knowledge_competency', $completed);
+
+  }
+
+  // Adds 'roles' data to revisions.
+  if ($sandbox['progress'] == 4) {
+
+    $database = \Drupal::database();
+    if (!isset($sandbox['competency_ids'])) {
+      $database->update('knowledge_competency')
+        ->isNull('completed')
+        ->isNotNull('publisher_proposed')
+        ->expression('completed', 'publisher_proposed')
+        ->execute();
+
+      $sandbox['competency_ids'] = \Drupal::entityQuery('knowledge_competency')
+        ->accessCheck(FALSE)
+        ->execute();
+
+      $sandbox['competency_total'] = count($sandbox['competency_ids']);
+    }
+    else {
+
+      $competency_id = array_pop($sandbox['competency_ids']);
+      $query = $database->select('knowledge_competency_revision', 'kcr');
+      $query->addField('kcr', 'vid');
+      $query->condition('id', $competency_id);
+      $revision_ids = $query->execute()->fetchAll();
+
+      $competency_storage = \Drupal::entityTypeManager()->getStorage('knowledge_competency');
+      $entity = $competency_storage->load($competency_id);
+
+      $score = [
+        'knowledge_candidate' => [
+          'correct' => 0,
+          'total' => 0,
+        ],
+        'knowledge_contributor' => [
+          'correct' => 0,
+          'total' => 0,
+        ],
+        'knowledge_publisher' => [
+          'correct' => 0,
+          'total' => 0,
+        ],
+      ];
+
+      foreach ($fields as $field => $role) {
+        $score[$role]['total'] += 1;
+        if ($entity->get($field)->value) {
+          $score[$role]['correct'] += 1;
+        }
+        $score[$role]['completed'] = ($score[$role]['correct'] == $score[$role]['total'])
+          ? $entity->get('revision_timestamp')->value
+          : NULL;
+      }
+
+      $database->insert('knowledge_competency__roles')
+        ->fields([
+          'bundle',
+          'deleted',
+          'entity_id',
+          'langcode',
+          'delta',
+          'roles_role',
+          'roles_correct',
+          'roles_total',
+          'roles_proposer',
+          'roles_approver',
+          'roles_proposed',
+          'roles_approved',
+          'revision_id',
+        ])
+        ->values([
+          'bundle' => 'knowledge_competency',
+          'deleted' => 0,
+          'entity_id' => $competency_id,
+          'langcode' => 'en',
+          'delta' => 0,
+          'roles_role' => 'knowledge_candidate',
+          'roles_correct' => $score['knowledge_candidate']['correct'],
+          'roles_total' => $score['knowledge_candidate']['total'],
+          'roles_proposer' => NULL,
+          'roles_approver' => NULL,
+          'roles_proposed' => $score['knowledge_candidate']['completed'],
+          'roles_approved' => $score['knowledge_candidate']['completed'],
+          'revision_id' => $entity->getRevisionId(),
+        ])
+        ->values([
+          'bundle' => 'knowledge_competency',
+          'deleted' => 0,
+          'entity_id' => $competency_id,
+          'langcode' => 'en',
+          'delta' => 1,
+          'roles_role' => 'knowledge_contributor',
+          'roles_correct' => $score['knowledge_contributor']['correct'],
+          'roles_total' => $score['knowledge_contributor']['total'],
+          'roles_proposer' => NULL,
+          'roles_approver' => NULL,
+          'roles_proposed' => $score['knowledge_contributor']['completed'],
+          'roles_approved' => $score['knowledge_contributor']['completed'],
+          'revision_id' => $entity->getRevisionId(),
+        ])
+        ->values([
+          'bundle' => 'knowledge_competency',
+          'deleted' => 0,
+          'entity_id' => $competency_id,
+          'langcode' => 'en',
+          'delta' => 2,
+          'roles_role' => 'knowledge_publisher',
+          'roles_correct' => $score['knowledge_publisher']['correct'],
+          'roles_total' => $score['knowledge_publisher']['total'],
+          'roles_proposer' => $entity->get('publisher_coach')?->target_id,
+          'roles_approver' => $entity->get('publisher_leader')?->target_id,
+          'roles_proposed' => $entity->get('publisher_proposed')->value,
+          'roles_approved' => $entity->get('publisher_approved')->value,
+          'revision_id' => $entity->getRevisionId(),
+        ])->execute();
+
+      foreach ($revision_ids as $id => $vid) {
+        $revision = $competency_storage->loadRevision($vid->vid);
+        $score = [
+          'knowledge_candidate' => [
+            'correct' => 0,
+            'total' => 0,
+          ],
+          'knowledge_contributor' => [
+            'correct' => 0,
+            'total' => 0,
+          ],
+          'knowledge_publisher' => [
+            'correct' => 0,
+            'total' => 0,
+          ],
+        ];
+
+        foreach ($fields as $field => $role) {
+          $score[$role]['total'] += 1;
+          if ($revision->get($field)->value) {
+            $score[$role]['correct'] += 1;
+          }
+          $score[$role]['completed'] = ($score[$role]['correct'] == $score[$role]['total'])
+            ? $revision->get('revision_timestamp')->value
+            : NULL;
+        }
+
+        $database->insert('knowledge_competency_revision__roles')
+          ->fields([
+            'bundle',
+            'deleted',
+            'entity_id',
+            'langcode',
+            'delta',
+            'roles_role',
+            'roles_correct',
+            'roles_total',
+            'roles_proposer',
+            'roles_approver',
+            'roles_proposed',
+            'roles_approved',
+            'revision_id',
+          ])
+          ->values([
+            'bundle' => 'knowledge_competency',
+            'deleted' => 0,
+            'entity_id' => $competency_id,
+            'langcode' => 'en',
+            'delta' => 0,
+            'roles_role' => 'knowledge_candidate',
+            'roles_correct' => $score['knowledge_candidate']['correct'],
+            'roles_total' => $score['knowledge_candidate']['total'],
+            'roles_proposer' => NULL,
+            'roles_approver' => NULL,
+            'roles_proposed' => $score['knowledge_candidate']['completed'],
+            'roles_approved' => $score['knowledge_candidate']['completed'],
+            'revision_id' => $vid->vid,
+          ])
+          ->values([
+            'bundle' => 'knowledge_competency',
+            'deleted' => 0,
+            'entity_id' => $competency_id,
+            'langcode' => 'en',
+            'delta' => 1,
+            'roles_role' => 'knowledge_contributor',
+            'roles_correct' => $score['knowledge_contributor']['correct'],
+            'roles_total' => $score['knowledge_contributor']['total'],
+            'roles_proposer' => NULL,
+            'roles_approver' => NULL,
+            'roles_proposed' => $score['knowledge_contributor']['completed'],
+            'roles_approved' => $score['knowledge_contributor']['completed'],
+            'revision_id' => $vid->vid,
+          ])
+          ->values([
+            'bundle' => 'knowledge_competency',
+            'deleted' => 0,
+            'entity_id' => $competency_id,
+            'langcode' => 'en',
+            'delta' => 2,
+            'roles_role' => 'knowledge_publisher',
+            'roles_correct' => $score['knowledge_publisher']['correct'],
+            'roles_total' => $score['knowledge_publisher']['total'],
+            'roles_proposer' => $entity->get('publisher_coach')?->target_id,
+            'roles_approver' => $entity->get('publisher_leader')?->target_id,
+            'roles_proposed' => $entity->get('publisher_proposed')->value,
+            'roles_approved' => $entity->get('publisher_approved')->value,
+            'revision_id' => $vid->vid,
+          ])->execute();
+
+      }
+    }
+    $remaining_competency = count($sandbox['competency_ids']);
+    if ($remaining_competency) {
+      $competency_total = $sandbox['competency_total'];
+      $sandbox['#finished'] = ($competency_total - $remaining_competency) / $competency_total;
+      return;
+    }
+
+  }
+
+  $sandbox['progress'] += 1;
+  $sandbox['#finished'] = empty($sandbox['max']) ? 1 : $sandbox['progress'] / $sandbox['max'];
+
+  // Updates existing field definitions.
+  if ($sandbox['progress'] == 6) {
+    $sandbox = [];
+
+    \Drupal::entityTypeManager()->clearCachedDefinitions();
+    $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+    $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
+
+    $entity_type = $definition_update_manager->getEntityType('knowledge_competency');
+    $entity_type->set('class', 'Drupal\knowledge\Entity\Competency');
+    $entity_type->set('admin_permission', 'administer knowledge_competency');
+
+    $handlers = $entity_type->get('handlers');
+    $handlers['storage'] = 'Drupal\knowledge\CompetencyStorage';
+    $handlers['list_builder'] = 'Drupal\knowledge\CompetencyListBuilder';
+    $handlers['views_data'] = 'Drupal\knowledge\CompetencyViewsData';
+    $handlers['form'] = [
+      'default' => 'Drupal\knowledge\Form\CompetencyForm',
+      'add' => 'Drupal\knowledge\Form\CompetencyForm',
+      'edit' => 'Drupal\knowledge\Form\CompetencyForm',
+      'delete' => 'Drupal\knowledge\Form\CompetencyDeleteForm',
+      'approve' => 'Drupal\knowledge\Form\CompetencyApproveForm',
+    ];
+    $handlers['route_provider']['html'] = 'Drupal\knowledge\CompetencyHtmlRouteProvider';
+    $handlers['access'] = 'Drupal\knowledge\CompetencyAccessControlHandler';
+    $entity_type->set('handlers', $handlers);
+
+    $field_storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions('knowledge_competency');
+
+    $field_storage_definitions['user_id']->setDisplayConfigurable('form', FALSE);
+
+    $field_storage_definitions['contributor_coach']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_coach']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['contributor_leader']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_leader']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_coach']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_coach']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_leader']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_leader']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['contributor_proposed']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_proposed']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['contributor_approved']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_approved']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_proposed']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_proposed']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_approved']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_approved']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['candidate_correct']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['candidate_correct']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['candidate_total']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['candidate_total']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['contributor_correct']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_correct']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['contributor_total']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['contributor_total']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_correct']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_correct']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['publisher_total']->setDisplayConfigurable('form', FALSE);
+    $field_storage_definitions['publisher_total']->setDisplayConfigurable('view', FALSE);
+
+    $field_storage_definitions['correct']->setDisplayConfigurable('view', TRUE);
+    $field_storage_definitions['total']->setDisplayConfigurable('view', TRUE);
+
+    $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);
+  }
+
+}
diff --git a/knowledge.libraries.yml b/knowledge.libraries.yml
index cc663a7639f68324d1fc5c8218724c8766e6f6ce..662dc4aba79151952f9ddce713978b2093de1950 100644
--- a/knowledge.libraries.yml
+++ b/knowledge.libraries.yml
@@ -88,6 +88,12 @@ competency_form:
   css:
     layout:
       css/competency-form.css: {}
+  js:
+    js/competency-form.js: {}
+  dependencies:
+    - core/drupal.entity-form
+    - core/jquery
+    - core/once
 
 competency_report:
   version: VERSION
@@ -100,3 +106,19 @@ competency_report:
     - core/jquery
     - core/once
     - moderation_dashboard/chart.js.external
+
+competency.admin:
+  version: VERSION
+  js:
+    js/knowledge.competency.admin.js: {}
+  css:
+    theme:
+      css/competency.admin.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.announce
+    - core/drupal.debounce
+    - core/drupal.dialog.ajax
+    - core/drupal.tabledrag
+    - core/once
\ No newline at end of file
diff --git a/knowledge.links.task.yml b/knowledge.links.task.yml
index 3c2b356a198de7cb9d5e42f09e0970f25551312d..9ba6b03dd885e9ce139ea13b71797f276b4e0680 100644
--- a/knowledge.links.task.yml
+++ b/knowledge.links.task.yml
@@ -115,6 +115,17 @@ knowledge_competency.settings_tab:
   title: 'Settings'
   base_route: knowledge_competency.settings
 
+knowledge_competency.settings:
+  title: 'Competencies'
+  route_name: knowledge_competency.settings
+  parent_id: knowledge_competency.settings_tab
+  weight: -10
+
+knowledge_competency.settings.role:
+  title: 'Roles'
+  route_name: knowledge_competency.settings.role
+  parent_id: knowledge_competency.settings_tab
+
 entity.knowledge_competency.canonical:
   route_name: entity.knowledge_competency.canonical
   base_route: entity.knowledge_competency.canonical
@@ -141,6 +152,12 @@ entity.user.knowledge_competency_tab:
   title: 'Competency'
   base_route: entity.user.canonical
 
+entity.knowledge_competency.collection:
+  title: 'Competency'
+  route_name: entity.knowledge_competency.collection
+  parent_id: knowledge.admin
+  weight: 20
+
 entity.knowledge_wave.view:
   title: 'View'
   route_name: entity.knowledge_wave.canonical
diff --git a/knowledge.module b/knowledge.module
index 3a84a9c9f3444468912f58a0bacdee6c531c1261..245901f303581448d3e7d135ad5a61b81f2082dc 100644
--- a/knowledge.module
+++ b/knowledge.module
@@ -927,7 +927,7 @@ function knowledge_library_info_alter(&$libraries, $extension) {
 /**
  * Allowed values for the audience field.
  */
-function knowledge_audience_allowed_values(FieldStorageDefinitionInterface $definition, FieldableEntityInterface $entity = NULL) {
+function knowledge_audience_allowed_values(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL) {
 
   $has_permission = \Drupal::currentUser()
     ->hasPermission('create knowledge audience external');
@@ -982,7 +982,7 @@ function knowledge_node_access(NodeInterface $node, $op, AccountInterface $accou
       }
 
       if ($node->hasField('knowledge_governance') && $node->get('knowledge_governance')->value) {
-        if (!empty(array_intersect($account->getRoles(), $user_roles))) {
+        if (!empty(array_intersect($account->getRoles(TRUE), $user_roles))) {
           return AccessResult::allowed();
         }
         elseif (in_array($account->id(), $users)) {
@@ -1046,10 +1046,13 @@ function knowledge_node_presave(EntityInterface $entity) {
  * Implements hook_ENTITY_TYPE_ID_presave().
  */
 function knowledge_user_presave(EntityInterface $entity) {
+  /** @var \Drupal\user\UserInterface $user */
+  $user = $entity;
+
+  \Drupal::service('knowledge.competency')->doRoleRemoval($user);
 
   try {
-    /** @var \Drupal\user\UserInterface $user */
-    $user = $entity;
+
     $leadership_changed = FALSE;
     if ($user->isNew()) {
       $leadership_changed = TRUE;
@@ -1064,7 +1067,7 @@ function knowledge_user_presave(EntityInterface $entity) {
 
       if ($user->get('knowledge_leader')->target_id) {
         $leader = $user->get('knowledge_leader')->entity;
-        $roles = $leader->getRoles();
+        $roles = $leader->getRoles(TRUE);
         if (!in_array('knowledge_leader', $roles)) {
           $leader_svc->addKnowledgeLeader($leader);
         }
@@ -1108,7 +1111,7 @@ function knowledge_user_delete(EntityInterface $entity) {
 /**
  * Implements hook_entity_field_access().
  */
-function knowledge_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+function knowledge_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
   $permission = NULL;
   $field_name = $field_definition->getName();
 
diff --git a/knowledge.permissions.yml b/knowledge.permissions.yml
index e2653bd316ae4272c761812959c4e7b1f4746c85..46a58cf5855720ab9617adfb4c92178768d8c72d 100644
--- a/knowledge.permissions.yml
+++ b/knowledge.permissions.yml
@@ -1,16 +1,22 @@
 administer knowledge:
   title: 'Administer knowledge and knowledge settings'
+
 administer knowledge types:
   title: 'Administer knowledge types and settings'
   restrict access: true
+
 access knowledge:
   title: 'View knowledge'
+
 post knowledge:
   title: 'Post knowledge'
+
 skip knowledge approval:
   title: 'Skip knowledge approval'
+
 edit own knowledge:
   title: 'Edit own knowledge'
+
 edit own learners field:
   title: 'Edit own learners field'
 
@@ -68,36 +74,58 @@ delete all quality revisions:
 access knowledge reports:
   title: 'Access knowledge reports'
   description: 'Access knowledge reports'
-add competency entities:
-  title: 'Create new Competency entities'
 
-administer competency entities:
-  title: 'Administer Competency entities'
+add knowledge_competency:
+  title: 'Create new Competency'
+
+administer knowledge_competency:
+  title: 'Administer Competency'
   description: 'Allow to access the administration form to configure Competency entities.'
   restrict access: true
 
-delete competency entities:
-  title: 'Delete Competency entities'
+edit other knowledge_competency:
+  title: "Edit Other's Competency"
+  description: 'Edit other users Competency. Can not edit own Competency.'
+
+edit own knowledge_competency:
+  title: 'Edit own Competency'
+  description: 'Competency can change roles. A users should not have control over their comptency.'
+  restrict access: true
+
+edit learner knowledge_competency:
+  title: 'Edit Learners Competency'
+  description: 'If you are their coach'
+
+view own knowledge_competency:
+  title: 'View Own Competency'
+
+view learner knowledge_competency:
+  title: 'View Learner Competency'
+  description: 'If you are their coach'
+
+view follower knowledge_competency:
+  title: 'View Follower Competency'
+  description: 'If you are their leader'
 
-edit competency entities:
-  title: 'Edit Competency entities'
+view any knowledge_competency:
+  title: 'View Any Competency'
 
-view published competency entities:
-  title: 'View published Competency entities'
+view published knowledge_competency:
+  title: 'View published Competency'
 
-view unpublished competency entities:
-  title: 'View unpublished Competency entities'
+view unpublished knowledge_competency:
+  title: 'View unpublished Competency'
 
-view all competency revisions:
+view all knowledge_competency revisions:
   title: 'View all Competency revisions'
 
-revert all competency revisions:
+revert all knowledge_competency revisions:
   title: 'Revert all Competency revisions'
-  description: 'Role requires permission <em>view Competency revisions</em> and <em>edit rights</em> for competency entities in question or <em>administer competency entities</em>.'
+  description: 'Role requires permission <em>view Competency revisions</em> and <em>edit rights</em> for competency entities in question or <em>administer knowledge_competency</em>.'
 
-delete all competency revisions:
+delete all knowledge_competency revisions:
   title: 'Delete all revisions'
-  description: 'Role requires permission to <em>view Competency revisions</em> and <em>delete rights</em> for competency entities in question or <em>administer competency entities</em>.'
+  description: 'Role requires permission to <em>view Competency revisions</em> and <em>delete rights</em> for competency entities in question or <em>administer knowledge_competency</em>.'
 
 # permission_callbacks:
 #   - \Drupal\knowledge\KnowledgePermissions::permissions
diff --git a/knowledge.routing.yml b/knowledge.routing.yml
index 6212e55eec928d73e84be59e4455ca86ddcb886e..09519234fcc1c4e68788efa70e5bee5ed6b5c378 100644
--- a/knowledge.routing.yml
+++ b/knowledge.routing.yml
@@ -185,28 +185,32 @@ knowledge.admin_reports:
 entity.user.knowledge_competency:
   path: '/user/{user}/competency'
   defaults:
-    _controller: '\Drupal\knowledge\Controller\KnowledgeCompetencyController::userCompetency'
+    _controller: '\Drupal\knowledge\Controller\CompetencyController::userCompetency'
   requirements:
-    _permission: 'view published competency entities'
-    user: \d+
+    _custom_access: '\Drupal\knowledge\Controller\CompetencyController::accessUserCompetency'
   options:
     _admin_route: TRUE
+    parameters:
+      user:
+        type: entity:user
 
 entity.user.knowledge_competency.approval_form:
   path: '/user/{user}/competency/approve'
   defaults:
-    _form: '\Drupal\knowledge\Form\KnowledgeCompetencyApproveForm'
+    _form: '\Drupal\knowledge\Form\CompetencyApproveForm'
     _title: 'Approval'
   requirements:
-    _custom_access: '\Drupal\knowledge\Controller\KnowledgeCompetencyController::accessCompetencyApproval'
-    user: \d+
+    _custom_access: '\Drupal\knowledge\Controller\CompetencyController::accessCompetencyApproval'
   options:
     _admin_route: TRUE
+    parameters:
+      user:
+        type: entity:user
 
 knowledge.report.competency:
   path: '/admin/reports/knowledge/competency'
   defaults:
-    _controller: '\Drupal\knowledge\Controller\KnowledgeCompetencyController::competencySummaryReport'
+    _controller: '\Drupal\knowledge\Controller\CompetencyController::competencySummaryReport'
     _title: 'Knowledge'
   requirements:
     _permission: 'access knowledge reports'
diff --git a/knowledge.services.yml b/knowledge.services.yml
index d007a511f6f387c5457dfdd17c4dab57b69c6e47..c935ed1fc1ed9a6918b41fdad1a6fce480dfed5b 100644
--- a/knowledge.services.yml
+++ b/knowledge.services.yml
@@ -73,3 +73,11 @@ services:
     class: Drupal\knowledge\Service\KnowledgeLinkRelationship
     arguments:
       - '@entity_type.manager'
+
+  knowledge.competency:
+      class: '\Drupal\knowledge\Service\CompetencyService'
+      arguments:
+        - '@config.factory'
+        - '@entity_type.manager'
+        - '@string_translation'
+        - '@messenger'
diff --git a/modules/knowledge_field/config/schema/knowledge_field.schema.yml b/modules/knowledge_field/config/schema/knowledge_field.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e63fafcb4715ea2b46a66af57392355cb35a7279
--- /dev/null
+++ b/modules/knowledge_field/config/schema/knowledge_field.schema.yml
@@ -0,0 +1,42 @@
+field.value.knowledge_competency_role:
+  type: mapping
+  label: Default value
+  mapping:
+    role:
+      type: label
+      label: Role
+    correct:
+      type: integer
+      label: Correct
+    total:
+      type: integer
+      label: Total
+    proposer:
+      type: integer
+      label: Proposer
+    proposed:
+      type: integer
+      label: Proposed
+    approver:
+      type: integer
+      label: Approver
+    approved:
+      type: integer
+      label: Approved
+
+
+field.formatter.settings.knowledge_competency_role:
+  type: mapping
+  label: Competency Role formatter settings
+  mapping:
+    foo:
+      type: string
+      label: Foo
+
+field.widget.settings.knowledge_competency_role:
+  type: mapping
+  label: Competency Role widget settings
+  mapping:
+    foo:
+      type: string
+      label: Foo
diff --git a/modules/knowledge_field/knowledge_field.info.yml b/modules/knowledge_field/knowledge_field.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5eff247116db312dc95508a9786f1cab5bd1ae1a
--- /dev/null
+++ b/modules/knowledge_field/knowledge_field.info.yml
@@ -0,0 +1,5 @@
+name: 'Knowledge fields'
+type: module
+description: 'Custom fields used by knowledge entities.'
+package: Knowledge
+core_version_requirement: ^10
diff --git a/modules/knowledge_field/knowledge_field.module b/modules/knowledge_field/knowledge_field.module
new file mode 100644
index 0000000000000000000000000000000000000000..dc7c6cf909e43e2b81e723f11ba152e9ad8d06b2
--- /dev/null
+++ b/modules/knowledge_field/knowledge_field.module
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * @file
+ * Contains .
+ */
+
+declare(strict_types=1);
diff --git a/modules/knowledge_field/src/Helper/CompetencyField.php b/modules/knowledge_field/src/Helper/CompetencyField.php
new file mode 100644
index 0000000000000000000000000000000000000000..311b544e72e5e27e5a99a2a2efe85321fba4980b
--- /dev/null
+++ b/modules/knowledge_field/src/Helper/CompetencyField.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\knowledge_field\Helper;
+
+/**
+ * Helper class for the knowledge competency entity.
+ */
+class CompetencyField {
+
+  /**
+   * Get the field definitions for the knowledge competency entity.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
+   *   The field definitions.
+   *
+   * @return array
+   *   The field definitions for the knowledge competency entity.
+   */
+  public static function roleFields($field_definitions) {
+    $fields = [];
+    foreach ($field_definitions as $field_name => $field_definition) {
+      if ($field_definition->getType() != 'boolean') {
+        continue;
+      }
+      if (get_class($field_definition) != 'Drupal\field\Entity\FieldConfig') {
+        continue;
+      }
+      $role = $field_definition->getThirdPartySetting('knowledge', 'competency_role', '_none');
+      if ($role == '_none') {
+        continue;
+      }
+      $fields[$role][] = $field_name;
+    }
+
+    return $fields;
+  }
+
+}
diff --git a/modules/knowledge_field/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyRoleFormatter.php b/modules/knowledge_field/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyRoleFormatter.php
new file mode 100644
index 0000000000000000000000000000000000000000..dd8f9d5336551ceaf0c089b9fce7dea25ebe4a62
--- /dev/null
+++ b/modules/knowledge_field/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyRoleFormatter.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\knowledge_field\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Plugin implementation of the 'Competency Role' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "knowledge_competency_role",
+ *   label = @Translation("Competency Role"),
+ *   field_types = {"knowledge_competency_role"},
+ * )
+ */
+final class KnowledgeCompetencyRoleFormatter extends FormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings(): array {
+    $setting = ['foo' => 'bar'];
+    return $setting + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $elements['foo'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Foo'),
+      '#default_value' => $this->getSetting('foo'),
+    ];
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary(): array {
+    return [
+      $this->t('Foo: @foo', ['@foo' => $this->getSetting('foo')]),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode): array {
+    $element = [];
+    foreach ($items as $delta => $item) {
+      $element[$delta] = [
+        '#markup' => $item->value,
+      ];
+    }
+    return $element;
+  }
+
+}
diff --git a/modules/knowledge_field/src/Plugin/Field/FieldType/KnowledgeCompetencyRoleItem.php b/modules/knowledge_field/src/Plugin/Field/FieldType/KnowledgeCompetencyRoleItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..760d1f8f414805bf23bfc4a03e4ffba91d9faa37
--- /dev/null
+++ b/modules/knowledge_field/src/Plugin/Field/FieldType/KnowledgeCompetencyRoleItem.php
@@ -0,0 +1,227 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\knowledge_field\Plugin\Field\FieldType;
+
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\knowledge_field\Helper\CompetencyField;
+
+/**
+ * Defines the 'knowledge_competency_role' field type.
+ *
+ * @FieldType(
+ *   id = "knowledge_competency_role",
+ *   label = @Translation("Competency Roles"),
+ *   description = @Translation("Tracks role progress."),
+ *   default_widget = "knowledge_competency_role",
+ *   default_formatter = "knowledge_competency_role",
+ *   no_ui = TRUE,
+ * )
+ */
+final class KnowledgeCompetencyRoleItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function mainPropertyName() {
+    return 'role';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty(): bool {
+    return match ($this->get('role')->getValue()) {
+      NULL, '' => TRUE,
+      default => FALSE,
+    };
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultStorageSettings() {
+    return [
+      'target_type' => 'user',
+      'title' => '',
+    ] + parent::defaultStorageSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultFieldSettings() {
+    return [
+      'handler' => 'default',
+      'target_type' => 'user',
+      'handler_settings' => [],
+    ] + parent::defaultFieldSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetDefinition() {
+    return $this->targetDefinition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
+
+    // @DCG
+    // See /core/lib/Drupal/Core/TypedData/Plugin/DataType directory for
+    // available data types.
+    $properties['role'] = DataDefinition::create('string')
+      ->setLabel(t('Role'))
+      ->setRequired(TRUE);
+
+    $properties['correct'] = DataDefinition::create('integer')
+      ->setLabel(t('Correct'))
+      ->setRequired(TRUE);
+
+    $properties['total'] = DataDefinition::create('integer')
+      ->setLabel(t('Total'))
+      ->setRequired(TRUE);
+
+    $properties['proposer'] = DataDefinition::create('entity_reference')
+      ->setLabel(t('Proposer'))
+      ->setSetting('target_type', 'user')
+      ->setRequired(FALSE);
+
+    $properties['approver'] = DataDefinition::create('entity_reference')
+      ->setLabel(t('Approver'))
+      ->setSetting('target_type', 'user')
+      ->setRequired(FALSE);
+
+    $properties['proposed'] = DataDefinition::create('integer')
+      ->setLabel(t('Proposed'))
+      ->setRequired(FALSE);
+
+    $properties['approved'] = DataDefinition::create('integer')
+      ->setLabel(t('Approved'))
+      ->setRequired(FALSE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraints(): array {
+    $constraints = parent::getConstraints();
+
+    $constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager();
+
+    // @DCG Suppose our value must not be longer than 10 characters.
+    // $options['value']['Length']['max'] = 10;
+    // @DCG
+    // See /core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint
+    // directory for available constraints.
+    // $constraints[] = $constraint_manager->create('ComplexData', $options);
+    return $constraints;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition): array {
+
+    $columns = [
+      'role' => [
+        'type' => 'varchar',
+        'not null' => FALSE,
+        'description' => 'Column description.',
+        'length' => 255,
+      ],
+      'correct' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'description' => 'Column description.',
+      ],
+      'total' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'description' => 'Column description.',
+      ],
+      'proposer' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'unsigned' => TRUE,
+        'description' => 'The user who proposed the advancement.',
+      ],
+      'approver' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'unsigned' => TRUE,
+        'description' => 'The users that appoved the advacement.',
+      ],
+      'proposed' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'description' => 'When advancement is proposed.',
+      ],
+      'approved' => [
+        'type' => 'int',
+        'not null' => FALSE,
+        'description' => 'When advancement is appoved.',
+      ],
+
+    ];
+
+    $schema = [
+      'columns' => $columns,
+      // @todo Add indexes here if necessary.
+    ];
+
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave() {
+    $entity = $this->getEntity();
+    $definitions = $entity->getFieldDefinitions();
+    $roles = CompetencyField::roleFields($definitions);
+    $role = $this->values['role'];
+    $fields = $roles[$role] ?? [];
+    $this->total = 0;
+    $this->correct = 0;
+    foreach ($fields as $field) {
+      $value = $entity->get($field)->value;
+      if ($value) {
+        $this->correct += 1;
+      }
+      $this->total += 1;
+    }
+
+    parent::preSave();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function generateSampleValue(FieldDefinitionInterface $field_definition): array {
+    $random = new Random();
+    $values['role'] = $random->word(mt_rand(1, 50));
+    return $values;
+  }
+
+  /**
+   * Is the field item pending approval.
+   *
+   * @return bool
+   *   TRUE if the field item is pending approval.
+   */
+  public function isPending(): bool {
+    return $this->proposed !== NULL && $this->approved === NULL;
+  }
+
+}
diff --git a/modules/knowledge_field/src/Plugin/Field/FieldWidget/KnowledgeCompetencyRoleWidget.php b/modules/knowledge_field/src/Plugin/Field/FieldWidget/KnowledgeCompetencyRoleWidget.php
new file mode 100644
index 0000000000000000000000000000000000000000..15f024ff2bf62606833fad3f70bd088afd7406cf
--- /dev/null
+++ b/modules/knowledge_field/src/Plugin/Field/FieldWidget/KnowledgeCompetencyRoleWidget.php
@@ -0,0 +1,286 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\knowledge_field\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\knowledge\KnowledgeCompetencyInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Defines the 'knowledge_competency_role' field widget.
+ *
+ * @FieldWidget(
+ *   id = "knowledge_competency_role",
+ *   label = @Translation("Competency Role"),
+ *   field_types = {"knowledge_competency_role"},
+ * )
+ */
+final class KnowledgeCompetencyRoleWidget extends WidgetBase {
+
+  /**
+   * The field definitions.
+   *
+   * @var array
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * The competency settings.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $competencySettings;
+
+  /**
+   * The user storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $userStorage;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Constructs a Competency Role widget.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the widget.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the widget is associated.
+   * @param array $settings
+   *   The widget settings.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   * @param array $fields_definitions
+   *   The field definitions.
+   * @param \Drupal\Core\Config\ImmutableConfig $competency_settings
+   *   The settings.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $user_storage
+   *   The user storage.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, array $fields_definitions, ImmutableConfig $competency_settings, EntityStorageInterface $user_storage, EventDispatcherInterface $event_dispatcher) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+    $this->fieldDefinitions = $fields_definitions;
+    $this->competencySettings = $competency_settings;
+    $this->userStorage = $user_storage;
+    $this->eventDispatcher = $event_dispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+        $plugin_id,
+        $plugin_definition,
+        $configuration['field_definition'],
+        $configuration['settings'],
+        $configuration['third_party_settings'],
+        $container->get('entity_field.manager')->getFieldDefinitions('knowledge_competency', 'knowledge_competency'),
+        $container->get('config.factory')->get('knowledge.competency.settings'),
+        $container->get('entity_type.manager')->getStorage('user'),
+        $container->get('event_dispatcher')
+      );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings(): array {
+    $setting = ['foo' => 'bar'];
+    return $setting + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $element['foo'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Foo'),
+      '#default_value' => $this->getSetting('foo'),
+    ];
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary(): array {
+    return [
+      $this->t('Foo: @foo', ['@foo' => $this->getSetting('foo')]),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
+
+    $element['role'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Role'),
+      '#default_value' => $items[$delta]->role ?? NULL,
+    ];
+    $element['correct'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Correct'),
+      '#default_value' => $items[$delta]->correct ?? NULL,
+    ];
+    $element['total'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Total'),
+      '#default_value' => $items[$delta]->total ?? NULL,
+    ];
+
+    $coach = NULL;
+    if ($items[$delta]->coach) {
+      $coach = $this->userStorage->load($items[$delta]->coach);
+    }
+    $element['proposer'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('Coach'),
+      '#target_type' => 'user',
+      '#default_value' => $coach,
+    ];
+
+    $approver = NULL;
+    if ($items[$delta]->approver) {
+      $approver = $this->userStorage->load($items[$delta]->approver);
+    }
+    $element['approver'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('Approver'),
+      '#target_type' => 'user',
+      '#default_value' => $approver,
+    ];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state): array {
+    $form_values = $form_state->getValues();
+    $entity = $form_state->getFormObject()->getEntity();
+    $existing_values = $this->getExistingValues($entity);
+    $roles = $this->getFieldRoleValues($form_values);
+    $role_settings = $this->competencySettings->get('roles') ?? [];
+    $new_values = [];
+    $completed = [];
+    foreach ($role_settings as $i => $setting) {
+      $role = $setting['role'];
+      if (empty($role)) {
+        continue;
+      }
+      if (!isset($existing_values[$role])) {
+        $existing_values[$role] = [
+          'correct' => 0,
+          'total' => 0,
+          'role' => $role,
+          'proposer' => NULL,
+          'approver' => NULL,
+          'proposed' => NULL,
+          'approved' => NULL,
+        ];
+      }
+      $value = [
+        'role' => $role,
+        'correct' => $roles[$role]['correct'] ?? 0,
+        'total' => $roles[$role]['total'] ?? 0,
+        'proposer' => NULL,
+        'approver' => NULL,
+        'proposed' => $existing_values[$role]['proposed'] ?? NULL,
+        'approved' => $existing_values[$role]['approved'] ?? NULL,
+      ];
+      if ($existing_values[$role]['proposer']) {
+        $value['proposer'] = $this->userStorage->load($existing_values[$role]['proposer']);
+      }
+      if ($existing_values[$role]['approver']) {
+        $value['approver'] = $this->userStorage->load($existing_values[$role]['approver']);
+      }
+      $complete = ($value['correct'] == $value['total']);
+      $existing_complete = $existing_values[$role]['proposer'];
+
+      $new_values[] = $value;
+    }
+
+    return $new_values;
+  }
+
+  /**
+   * Get the field role.
+   *
+   * @return array
+   *   The field role.
+   */
+  private function getFieldRoleValues($form_values) {
+    $roles = [];
+    foreach ($this->fieldDefinitions as $field_name => $field_definition) {
+      if ($field_definition->getType() != 'boolean') {
+        continue;
+      }
+      if ($field_name == 'revision_default') {
+        continue;
+      }
+      $role = $field_definition->getThirdPartySetting('knowledge', 'competency_role', '_none');
+      if ($role == '_none') {
+        continue;
+      }
+      if (!isset($roles[$role])) {
+        $roles[$role] = [
+          'correct' => 0,
+          'total' => 0,
+          'role' => $role,
+        ];
+      }
+      $roles[$role]['total'] += 1;
+      if ($form_values[$field_name]['value'] == 1) {
+        $roles[$role]['correct'] += 1;
+      }
+    }
+
+    return $roles;
+  }
+
+  /**
+   * Get the existing values.
+   */
+  private function getExistingValues(KnowledgeCompetencyInterface $competency) {
+    $roles = $competency->get('roles');
+    $existing = [];
+    foreach ($roles as $role) {
+      $r = $role->role;
+      $existing[$r] = [
+        'correct' => $role->correct,
+        'total' => $role->total,
+        'role' => $role->role,
+        'proposer' => $role->proposer,
+        'approver' => $role->approver,
+        'proposed' => $role->proposed,
+        'approved' => $role->approved,
+      ];
+    }
+
+    return $existing;
+  }
+
+}
diff --git a/src/CompetencyAccessControlHandler.php b/src/CompetencyAccessControlHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c73711170e25b89a378fb8016c1b8034e425ed6
--- /dev/null
+++ b/src/CompetencyAccessControlHandler.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\knowledge;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Access controller for the Competency entity.
+ *
+ * @see \Drupal\knowledge\Entity\Competency.
+ */
+class CompetencyAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\knowledge\KnowledgeCompetencyInterface $entity */
+
+    switch ($operation) {
+
+      case 'view':
+        $permissions = [
+          'view any knowledge_competency',
+        ];
+        $own = $entity->getOwnerId() == $account->id();
+        if ($own) {
+          $permissions[] = 'view own knowledge_competency';
+        }
+
+        $is_learner = $entity->getOwner()->get('knowledge_coach')->target_id == $account->id();
+        if ($is_learner) {
+          $permissions[] = 'view learner knowledge_competency';
+        }
+
+        $is_leader = $entity->getOwner()->get('knowledge_leader')->target_id == $account->id();
+        if ($is_leader) {
+          $permissions[] = 'view follower knowledge_competency';
+        }
+
+        return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
+
+      case 'update':
+        $permissions = [];
+        $owner = $entity->getOwner();
+        $account_id = $account->id();
+
+        if ($account_id == $owner->id()) {
+          $permissions[] = 'edit own knowledge_competency';
+        }
+        else {
+          $permissions[] = 'edit other knowledge_competency';
+        }
+
+        $coach_id = $owner->get('knowledge_coach')?->target_id;
+        if ($account_id == $coach_id) {
+          $permissions[] = 'edit learner knowledge_competency';
+        }
+
+        return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
+
+      case 'delete':
+
+        return AccessResult::allowedIfHasPermission($account, 'administer knowledge_competency');
+    }
+
+    // Unknown operation, no opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'add knowledge_competency');
+  }
+
+}
diff --git a/src/KnowledgeCompetencyHtmlRouteProvider.php b/src/CompetencyHtmlRouteProvider.php
similarity index 75%
rename from src/KnowledgeCompetencyHtmlRouteProvider.php
rename to src/CompetencyHtmlRouteProvider.php
index 67c5f619d78ebd20b1747063ae04bb852f61e58d..988bb32634eb8c820112b54211d97f1566f97370 100644
--- a/src/KnowledgeCompetencyHtmlRouteProvider.php
+++ b/src/CompetencyHtmlRouteProvider.php
@@ -12,7 +12,7 @@ use Symfony\Component\Routing\Route;
  * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
  * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
  */
-class KnowledgeCompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
+class CompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
 
   /**
    * {@inheritdoc}
@@ -42,6 +42,10 @@ class KnowledgeCompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
       $collection->add("$entity_type_id.settings", $settings_form_route);
     }
 
+    if ($role_form_route = $this->getRoleFormRoute($entity_type)) {
+      $collection->add("$entity_type_id.settings.role", $role_form_route);
+    }
+
     return $collection;
   }
 
@@ -60,7 +64,7 @@ class KnowledgeCompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
       $route
         ->setDefaults([
           '_title' => "{$entity_type->getLabel()} revisions",
-          '_controller' => '\Drupal\knowledge\Controller\KnowledgeCompetencyController::revisionOverview',
+          '_controller' => '\Drupal\knowledge\Controller\CompetencyController::revisionOverview',
         ])
         ->setRequirement('_permission', 'view all competency revisions')
         ->setOption('_admin_route', TRUE);
@@ -85,8 +89,8 @@ class KnowledgeCompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
       $route = new Route($entity_type->getLinkTemplate('revision'));
       $route
         ->setDefaults([
-          '_controller' => '\Drupal\knowledge\Controller\KnowledgeCompetencyController::revisionShow',
-          '_title_callback' => '\Drupal\knowledge\Controller\KnowledgeCompetencyController::revisionPageTitle',
+          '_controller' => '\Drupal\knowledge\Controller\CompetencyController::revisionShow',
+          '_title_callback' => '\Drupal\knowledge\Controller\CompetencyController::revisionPageTitle',
         ])
         ->setRequirement('_permission', 'view all competency revisions')
         ->setOption('_admin_route', TRUE);
@@ -159,20 +163,42 @@ class KnowledgeCompetencyHtmlRouteProvider extends AdminHtmlRouteProvider {
    *   The generated route, if available.
    */
   protected function getSettingsFormRoute(EntityTypeInterface $entity_type) {
-    if (!$entity_type->getBundleEntityType()) {
-      $route = new Route("/admin/structure/knowledge/competency/settings");
-      $route
-        ->setDefaults([
-          '_form' => 'Drupal\knowledge\Form\KnowledgeCompetencySettingsForm',
-          '_title' => "Competency",
-        ])
-        ->setRequirement('_permission', $entity_type->getAdminPermission())
-        ->setOption('_admin_route', TRUE);
 
-      return $route;
-    }
+    $route = new Route("/admin/structure/knowledge/competency/settings");
+    $route
+      ->setDefaults([
+        '_form' => 'Drupal\knowledge\Form\CompetencySettingsForm',
+        '_title' => "Competency",
+      ])
+      ->setRequirement('_permission', $entity_type->getAdminPermission())
+      ->setOption('_admin_route', TRUE);
+
+    return $route;
+
+  }
+
+  /**
+   * Gets the settings form route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRoleFormRoute(EntityTypeInterface $entity_type) {
+
+    $route = new Route("/admin/structure/knowledge/competency/settings/role");
+    $route
+      ->setDefaults([
+        '_form' => 'Drupal\knowledge\Form\CompetencyRoleForm',
+        '_title' => "Competency",
+      ])
+      ->setRequirement('_permission', $entity_type->getAdminPermission())
+      ->setOption('_admin_route', TRUE);
+
+    return $route;
 
-    return NULL;
   }
 
 }
diff --git a/src/KnowledgeCompetencyListBuilder.php b/src/CompetencyListBuilder.php
similarity index 84%
rename from src/KnowledgeCompetencyListBuilder.php
rename to src/CompetencyListBuilder.php
index ecd18440e68df8cbe09cf1347575f3c1dfe1c454..213e45c52127d52257c43593b1f17ce2049096a1 100644
--- a/src/KnowledgeCompetencyListBuilder.php
+++ b/src/CompetencyListBuilder.php
@@ -11,14 +11,14 @@ use Drupal\Core\Link;
  *
  * @ingroup knowledge
  */
-class KnowledgeCompetencyListBuilder extends EntityListBuilder {
+class CompetencyListBuilder extends EntityListBuilder {
 
   /**
    * {@inheritdoc}
    */
   public function buildHeader() {
     $header['id'] = $this->t('Competency ID');
-    $header['name'] = $this->t('Name');
+    $header['user'] = $this->t('User');
     return $header + parent::buildHeader();
   }
 
@@ -28,7 +28,7 @@ class KnowledgeCompetencyListBuilder extends EntityListBuilder {
   public function buildRow(EntityInterface $entity) {
     /** @var \Drupal\knowledge\Entity\KnowledgeCompetency $entity */
     $row['id'] = $entity->id();
-    $row['name'] = Link::createFromRoute(
+    $row['user'] = Link::createFromRoute(
       $entity->getOwner()->label(),
       'entity.knowledge_competency.edit_form',
       ['knowledge_competency' => $entity->id()]
diff --git a/src/CompetencyStorage.php b/src/CompetencyStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..4f2ab7044fc49359f1790aa11b940d696d2d46df
--- /dev/null
+++ b/src/CompetencyStorage.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\knowledge;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the storage handler class for Competency entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * Competency entities.
+ *
+ * @ingroup knowledge
+ */
+class CompetencyStorage extends SqlContentEntityStorage implements KnowledgeCompetencyStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revisionIds(KnowledgeCompetencyInterface $entity) {
+    return $this->database->query(
+      'SELECT vid FROM {knowledge_competency_revision} WHERE id=:id ORDER BY vid',
+      [':id' => $entity->id()]
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function userRevisionIds(AccountInterface $account) {
+    return $this->database->query(
+      'SELECT vid FROM {knowledge_competency_field_revision} WHERE uid = :uid ORDER BY vid',
+      [':uid' => $account->id()]
+    )->fetchCol();
+  }
+
+}
diff --git a/src/Controller/KnowledgeCompetencyController.php b/src/Controller/CompetencyController.php
similarity index 66%
rename from src/Controller/KnowledgeCompetencyController.php
rename to src/Controller/CompetencyController.php
index 988cb52c62715b90333ac995811b7df7fb088437..8624d43f36e183ee20fa917395bb112ef0ccc483 100644
--- a/src/Controller/KnowledgeCompetencyController.php
+++ b/src/Controller/CompetencyController.php
@@ -5,22 +5,24 @@ namespace Drupal\knowledge\Controller;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Link;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\TableSort;
-use Drupal\knowledge\Entity\KnowledgeCompetencyInterface;
+use Drupal\knowledge\KnowledgeCompetencyInterface;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
+use Drupal\user\UserInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * Class KnowledgeCompetencyController.
- *
- *  Returns responses for Competency routes.
+ * Competency Controller.
  */
-class KnowledgeCompetencyController extends ControllerBase implements ContainerInjectionInterface {
+class CompetencyController extends ControllerBase implements ContainerInjectionInterface {
 
   /**
    * The date formatter.
@@ -36,14 +38,38 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
    */
   protected $renderer;
 
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competency;
+
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
-    $instance = parent::create($container);
-    $instance->dateFormatter = $container->get('date.formatter');
-    $instance->renderer = $container->get('renderer');
-    return $instance;
+    return new static(
+      $container->get('date.formatter'),
+      $container->get('renderer'),
+      $container->get('knowledge.competency')
+    );
+  }
+
+  /**
+   * Constructs Competency Controller object.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
+   *   The date formatter.
+   * @param \Drupal\Core\Render\Renderer $renderer
+   *   The renderer.
+   * @param \Drupal\knowledge\KnowledgeCompetencyServiceInterface $competency
+   *   The competency service.
+   */
+  public function __construct(DateFormatter $date_formatter, Renderer $renderer, KnowledgeCompetencyServiceInterface $competency) {
+    $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
+    $this->competency = $competency;
   }
 
   /**
@@ -58,7 +84,7 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
   public function revisionShow($knowledge_competency_revision) {
     /** @var \Drupal\knowledge\KnowledgeCompetencyStorageInterface $competency_storage */
     $competency_storage = $this->entityTypeManager()->getStorage('knowledge_competency');
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $knowledge_competency */
+    /** @var \Drupal\knowledge\KnowledgeCompetencyInterface $knowledge_competency */
     $knowledge_competency = $competency_storage->loadRevision($knowledge_competency_revision);
     $view_builder = $this->entityTypeManager()->getViewBuilder('knowledge_competency');
 
@@ -77,7 +103,7 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
   public function revisionPageTitle($knowledge_competency_revision) {
     /** @var \Drupal\knowledge\KnowledgeCompetencyStorageInterface $competency_storage */
     $competency_storage = $this->entityTypeManager()->getStorage('knowledge_competency');
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $knowledge_competency */
+    /** @var \Drupal\knowledge\KnowledgeCompetencyInterface $knowledge_competency */
     $knowledge_competency = $competency_storage->loadRevision($knowledge_competency_revision);
 
     return $this->t('Revision of %title from %date', [
@@ -89,7 +115,7 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
   /**
    * Generates an overview table of older revisions of a Competency.
    *
-   * @param \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $knowledge_competency
+   * @param \Drupal\knowledge\KnowledgeCompetencyInterface $knowledge_competency
    *   A Competency object.
    *
    * @return array
@@ -103,8 +129,8 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
     $build['#title'] = $this->t('Revisions for %title', ['%title' => $knowledge_competency->label()]);
 
     $header = [$this->t('Revision'), $this->t('Operations')];
-    $revert_permission = (($account->hasPermission("revert all competency revisions") || $account->hasPermission('administer competency entities')));
-    $delete_permission = (($account->hasPermission("delete all competency revisions") || $account->hasPermission('administer competency entities')));
+    $revert_permission = (($account->hasPermission("revert all competency revisions") || $account->hasPermission('administer knowledge_competency')));
+    $delete_permission = (($account->hasPermission("delete all competency revisions") || $account->hasPermission('administer knowledge_competency')));
 
     $rows = [];
 
@@ -113,7 +139,7 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
     $latest_revision = TRUE;
 
     foreach (array_reverse($vids) as $vid) {
-      /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $revision */
+      /** @var \Drupal\knowledge\KnowledgeCompetencyInterface $revision */
       $revision = $knowledge_competency_storage->loadRevision($vid);
       $username = [
         '#theme' => 'username',
@@ -206,17 +232,30 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
 
   /**
    * Returns the competency the belongs to the user.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user to get the competency for.
    */
-  public function userCompetency($user) {
+  public function userCompetency(UserInterface $user) {
+    $user_id = $user->id();
     $account = $this->currentUser();
-    $view_builder = $this->entityTypeManager()->getViewBuilder('knowledge_competency');
-    $form_builder = $this->entityFormBuilder();
-    $competency = $this->getCompetency($user);
+    if ($user->get('knowledge_coach')->isEmpty()) {
+      return [
+        '#markup' => $this->t('@username does not have a coach.', ['@username' => $user->getDisplayName()]),
+      ];
+    }
+    $is_self = $account->id() == $user_id;
+    $is_learner = $account->id() == $user->get('knowledge_coach')->target_id || $account->id() == 1;
+
+    $competency = $this->competency->getUserCompetency($user_id);
 
-    if ($account->hasPermission('edit competency entities')) {
+    if ($account->hasPermission('edit learner knowledge_competency')) {
+      $form_builder = $this->entityFormBuilder();
       return $form_builder->getForm($competency);
     }
 
+    $view_builder = $this->entityTypeManager()->getViewBuilder('knowledge_competency');
+
     return $view_builder->view($competency);
   }
 
@@ -231,20 +270,44 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
    * @return \Drupal\Core\Access\AccessResultInterface
    *   The access result.
    */
-  public function accessCompetencyApproval(AccountInterface $account, int $user) {
+  public function accessUserCompetency(AccountInterface $account, UserInterface $user) {
 
-    $account_id = $account->id();
-    if ($account_id == 1) {
+    $competency = $this->competency->getUserCompetency($user->id());
+
+    if ($competency->access('view', $account)) {
+      return AccessResult::allowed();
+    }
+    if ($competency->access('update', $account)) {
       return AccessResult::allowed();
     }
-    $competency = $this->getCompetency($user);
-    if (!$competency->isPendingContributor() && !$competency->isPendingPublisher()) {
+
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Checks access for a specific request.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account (leader).
+   * @param \Drupal\user\UserInterface $user
+   *   The user the competency belongs to.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function accessCompetencyApproval(AccountInterface $account, UserInterface $user) {
+    $user_id = $user->id();
+    $competency = $this->competency->getUserCompetency($user_id);
+    $is_pending = $competency->isPending();
+    if (!$is_pending) {
       throw new NotFoundHttpException();
     }
 
-    $user = $this->entityTypeManager()
-      ->getStorage('user')
-      ->load($user);
+    $account_id = $account->id();
+    if ($account_id == 1) {
+      return AccessResult::allowed();
+    }
+
     $leader_id = $user->knowledge_leader->target_id;
 
     return AccessResult::allowedIf($account_id == $leader_id);
@@ -252,47 +315,52 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
 
   /**
    * The table report of competencies.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
    */
   public function competencySummaryReport(Request $request) {
 
-    $groups = field_group_info_groups('knowledge_competency', 'knowledge_competency', 'form', 'default');
-    $result = views_get_view_result('competency_progress');
+    $role_storage = $this->entityTypeManager()->getStorage('user_role');
+    $fields_definitions = \Drupal::service('entity_field.manager')
+      ->getFieldDefinitions('knowledge_competency', 'knowledge_competency');
+    $fields = [];
+    foreach ($fields_definitions as $field_id => $field_definition) {
+      if (get_class($field_definition) != 'Drupal\field\Entity\FieldConfig') {
+        continue;
+      }
+      $role = $field_definition->getThirdPartySetting('knowledge', 'competency_role', '_none');
+      if ($role == '_none') {
+        continue;
+      }
+      $fields[$field_id] = $role;
+    }
+
+    $result = views_get_view_result('knowledge_competency_progress');
+    if (empty($result)) {
+      return [];
+    }
     $result = $result[0];
     $entity = $result->_entity;
+    if (!$entity) {
+      return [];
+    }
     $total = $result->id;
     $rows = [];
     $order = 0;
-    $candidate = 'Paddler';
-    $contributor = 'Rider';
-    $publisher = 'Pro';
+
     $i = 0;
-    foreach ($groups['group_candidate']->children as $field) {
-      $competency = $entity->$field->getFieldDefinition()->getLabel();
-      $key = 'knowledge_competency__' . $field . '_' . $field . '_value';
-      $key = substr($key, 0, 60);
-      $value = $result->$key;
-      $percentage = (int) (100 * ($value / $total));
-      $i += 1;
-      $rows[] = [$i, $candidate, $competency, $percentage];
-    }
-    foreach ($groups['group_contributor']->children as $field) {
-      $competency = $entity->$field->getFieldDefinition()->getLabel();
-      $key = 'knowledge_competency__' . $field . '_' . $field . '_value';
-      $key = substr($key, 0, 60);
-      $value = $result->$key;
-      $percentage = (int) (100 * ($value / $total));
-      $i += 1;
-      $rows[] = [$i, $contributor, $competency, $percentage];
-    }
-    foreach ($groups['group_publisher']->children as $field) {
+    foreach ($fields as $field => $role_id) {
+      $role = $role_storage->load($role_id);
       $competency = $entity->$field->getFieldDefinition()->getLabel();
       $key = 'knowledge_competency__' . $field . '_' . $field . '_value';
       $key = substr($key, 0, 60);
       $value = $result->$key;
       $percentage = (int) (100 * ($value / $total));
       $i += 1;
-      $rows[] = [$i, $publisher, $competency, $percentage];
+      $rows[] = [$i, $role->label(), $competency, $percentage];
     }
+
     $header = [
       'id' => [
         'data' => $this->t('Id'),
@@ -348,6 +416,11 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
 
       $data['labels'][] = $competency;
       $data['datasets'][0]['data'][] = $percent;
+      if (!isset($colors[$role])) {
+        $color_array = $this->getColor($role);
+        $colors[$role] = 'rgba(' . $color_array[0] . ', ' . $color_array[1] . ', ' . $color_array[2];
+      }
+
       $data['datasets'][0]['backgroundColor'][] = $colors[$role] . ',.4)';
       $data['datasets'][0]['borderColor'][] = $colors[$role] . ',.9)';
     }
@@ -383,6 +456,11 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
 
   /**
    * Custom data sort.
+   *
+   * @param string $key
+   *   The key to sort by.
+   * @param bool $desc
+   *   Whether to sort in descending order.
    */
   private function tableSorter($key, $desc) {
     return function ($a, $b) use ($key, $desc) {
@@ -395,28 +473,19 @@ class KnowledgeCompetencyController extends ControllerBase implements ContainerI
   }
 
   /**
-   * Get the user's competency.
+   * Get a color based on a number.
    */
-  protected function getCompetency($user_id) {
-    $competency = NULL;
-    $storage = $this->entityTypeManager()
-      ->getStorage('knowledge_competency');
-    $query = $storage->getQuery();
-    $result = $query
-      ->condition('user_id', $user_id)
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if (count($result) == 1) {
-      $competency = $storage->load(current($result));
-    }
-    if (count($result) == 0) {
-      $competency = $storage->create([
-        'user_id' => $user_id,
-      ]);
-    }
-
-    return $competency;
+  public function getColor($num) {
+    // Modify 'color' to get a different palette.
+    $hash = md5('color' . $num);
+    return [
+    // R.
+      hexdec(substr($hash, 0, 2)),
+    // G.
+      hexdec(substr($hash, 2, 2)),
+          // B.
+      hexdec(substr($hash, 4, 2)),
+    ];
   }
 
 }
diff --git a/src/Entity/KnowledgeCompetency.php b/src/Entity/Competency.php
similarity index 65%
rename from src/Entity/KnowledgeCompetency.php
rename to src/Entity/Competency.php
index b8103e93874e607ed6e8cefc0443f670c625cdac..c6e409250cfb2a7bc25762e495f5c93a380482e7 100644
--- a/src/Entity/KnowledgeCompetency.php
+++ b/src/Entity/Competency.php
@@ -8,10 +8,13 @@ use Drupal\Core\Entity\EntityChangedTrait;
 use Drupal\Core\Entity\EntityConstraintViolationList;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\RevisionableInterface;
 use Drupal\Core\Entity\RevisionLogEntityTrait;
 use Drupal\Core\Entity\RevisionLogInterface;
+use Drupal\Core\Entity\RevisionableInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\knowledge\KnowledgeCompetencyInterface;
+use Drupal\knowledge_field\Helper\CompetencyField;
 use Drupal\user\UserInterface;
 use Symfony\Component\Validator\ConstraintViolation;
 
@@ -24,29 +27,29 @@ use Symfony\Component\Validator\ConstraintViolation;
  *   id = "knowledge_competency",
  *   label = @Translation("Competency"),
  *   handlers = {
- *     "storage" = "Drupal\knowledge\KnowledgeCompetencyStorage",
+ *     "storage" = "Drupal\knowledge\CompetencyStorage",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
- *     "list_builder" = "Drupal\knowledge\KnowledgeCompetencyListBuilder",
- *     "views_data" = "Drupal\knowledge\Entity\KnowledgeCompetencyViewsData",
+ *     "list_builder" = "Drupal\knowledge\CompetencyListBuilder",
+ *     "views_data" = "Drupal\knowledge\Entity\CompetencyViewsData",
  *
  *     "form" = {
- *       "default" = "Drupal\knowledge\Form\KnowledgeCompetencyForm",
- *       "add" = "Drupal\knowledge\Form\KnowledgeCompetencyForm",
- *       "edit" = "Drupal\knowledge\Form\KnowledgeCompetencyForm",
- *       "delete" = "Drupal\knowledge\Form\KnowledgeCompetencyDeleteForm",
- *       "approve" = "Drupal\knowledge\Form\KnowledgeCompetencyApproveForm",
+ *       "default" = "Drupal\knowledge\Form\CompetencyForm",
+ *       "add" = "Drupal\knowledge\Form\CompetencyForm",
+ *       "edit" = "Drupal\knowledge\Form\CompetencyForm",
+ *       "delete" = "Drupal\knowledge\Form\CompetencyDeleteForm",
+ *       "approve" = "Drupal\knowledge\Form\CompetencyApproveForm",
  *     },
  *     "route_provider" = {
- *       "html" = "Drupal\knowledge\KnowledgeCompetencyHtmlRouteProvider",
+ *       "html" = "Drupal\knowledge\CompetencyHtmlRouteProvider",
  *     },
- *     "access" = "Drupal\knowledge\KnowledgeCompetencyAccessControlHandler",
+ *     "access" = "Drupal\knowledge\CompetencyAccessControlHandler",
  *   },
  *   base_table = "knowledge_competency",
  *   revision_table = "knowledge_competency_revision",
  *   revision_data_table = "knowledge_competency_field_revision",
  *   show_revision_ui = TRUE,
  *   translatable = FALSE,
- *   admin_permission = "administer competency entities",
+ *   admin_permission = "administer knowledge_competency",
  *   entity_keys = {
  *     "id" = "id",
  *     "revision" = "vid",
@@ -73,7 +76,7 @@ use Symfony\Component\Validator\ConstraintViolation;
  *   field_ui_base_route = "knowledge_competency.settings"
  * )
  */
-class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInterface, RevisionLogInterface, KnowledgeCompetencyInterface {
+class Competency extends ContentEntityBase implements EntityChangedInterface, RevisionLogInterface, KnowledgeCompetencyInterface {
 
   use EntityChangedTrait;
   use RevisionLogEntityTrait;
@@ -98,6 +101,7 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
    * {@inheritdoc}
    */
   public function preSave(EntityStorageInterface $storage) {
+    $this->ensureRoles();
     parent::preSave($storage);
     $this->setNewRevision();
     // If no revision author has been set explicitly,
@@ -106,43 +110,67 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
       $this->setRevisionUserId($this->getOwnerId());
     }
 
-    $groups = field_group_info_groups('knowledge_competency', 'knowledge_competency', 'form', 'default');
-    $candidate_correct = 0;
-    $candidate_total = 0;
-    foreach ($groups['group_candidate']->children as $field) {
-      $candidate_total += 1;
-      if ($this->{$field}->value) {
-        $candidate_correct += 1;
-      }
+    $roles = $this->get('roles');
+    $total = 0;
+    $correct = 0;
+    foreach ($roles as $role) {
+      $total += $role->total;
+      $correct += $role->correct;
     }
-    $contributor_correct = 0;
-    $contributor_total = 0;
-    foreach ($groups['group_contributor']->children as $field) {
-      $contributor_total += 1;
-      if ($this->{$field}->value) {
-        $contributor_correct += 1;
-      }
+
+    $this->set('correct', $correct);
+    $this->set('total', $total);
+
+    $completed = NULL;
+    if ($correct == $total) {
+      $completed = time();
     }
-    $publisher_correct = 0;
-    $publisher_total = 0;
-    foreach ($groups['group_publisher']->children as $field) {
-      $publisher_total += 1;
-      if ($this->{$field}->value) {
-        $publisher_correct += 1;
-      }
+    $this->set('completed', $completed);
+
+    $this->setLegacyFields();
+
+    $roles = $this->get('roles');
+    $orginal_roles = $this->original?->get('roles')->getValue() ?? [];
+    \Drupal::service('knowledge.competency')->doPreSave($roles, $orginal_roles);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+    // Get the orginals values.
+    $orginal_roles = $this->original?->get('roles')->getValue() ?? [];
+    $roles = $this->get('roles')->getValue();
+    $owner = $this->getOwner();
+    \Drupal::service('knowledge.competency')->doPostSave($owner, $roles, $orginal_roles);
+
+  }
+
+  /**
+   * Ensure all roles are present.
+   */
+  private function ensureRoles() {
+    $roles = $this->get('roles');
+    $definitions = $this->getFieldDefinitions();
+    $role_fields = CompetencyField::roleFields($definitions);
+
+    foreach ($roles as $role) {
+      unset($role_fields[$role->role]);
     }
 
-    $this->set('candidate_correct', $candidate_correct);
-    $this->set('candidate_total', $candidate_total);
-    $this->set('contributor_correct', $contributor_correct);
-    $this->set('contributor_total', $contributor_total);
-    $this->set('publisher_correct', $publisher_correct);
-    $this->set('publisher_total', $publisher_total);
+    foreach ($role_fields as $role => $field) {
+      $roles->appendItem([
+        'role' => $role,
+        'correct' => 0,
+        'total' => 0,
+        'proposed' => NULL,
+        'approved' => NULL,
+        'proposer' => NULL,
+        'approver' => NULL,
+      ]);
+    }
 
-    $correct = $candidate_correct + $contributor_correct + $publisher_correct;
-    $total = $candidate_total + $contributor_total + $publisher_total;
-    $this->set('correct', $correct);
-    $this->set('total', $total);
   }
 
   /**
@@ -231,32 +259,33 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
           'placeholder' => '',
         ],
       ])
-      ->setDisplayConfigurable('form', TRUE)
       ->setDisplayConfigurable('view', TRUE);
 
-    $fields['contributor_coach'] = BaseFieldDefinition::create('entity_reference')
-      ->setLabel(t('Coach'))
-      ->setDescription(t('The user who suggests promotion to contributor.'))
-      ->setRevisionable(FALSE)
-      ->setSetting('target_type', 'user')
-      ->setSetting('handler', 'default')
+    $fields['roles'] = BaseFieldDefinition::create('knowledge_competency_role')
+      ->setLabel(t('Roles'))
+      ->setDescription(t('The roles of the user.'))
+      ->setRevisionable(TRUE)
+      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
       ->setDisplayOptions('view', [
         'label' => 'hidden',
-        'type' => 'author',
+        'type' => 'knowledge_competency_role',
         'weight' => 0,
       ])
       ->setDisplayOptions('form', [
-        'type' => 'entity_reference_autocomplete',
+        'type' => 'knowledge_competency_role',
         'weight' => 5,
-        'settings' => [
-          'match_operator' => 'CONTAINS',
-          'size' => '60',
-          'autocomplete_type' => 'tags',
-          'placeholder' => '',
-        ],
       ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayConfigurable('form', TRUE);
+
+    $fields['contributor_coach'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Coach'))
+      ->setDescription(t('The user who suggests promotion to contributor.'))
+      ->setRevisionable(FALSE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['contributor_leader'] = BaseFieldDefinition::create('entity_reference')
       ->setLabel(t('Leader'))
@@ -264,23 +293,8 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
       ->setRevisionable(FALSE)
       ->setSetting('target_type', 'user')
       ->setSetting('handler', 'default')
-      ->setDisplayOptions('view', [
-        'label' => 'hidden',
-        'type' => 'author',
-        'weight' => 0,
-      ])
-      ->setDisplayOptions('form', [
-        'type' => 'entity_reference_autocomplete',
-        'weight' => 5,
-        'settings' => [
-          'match_operator' => 'CONTAINS',
-          'size' => '60',
-          'autocomplete_type' => 'tags',
-          'placeholder' => '',
-        ],
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_coach'] = BaseFieldDefinition::create('entity_reference')
       ->setLabel(t('Coach'))
@@ -288,23 +302,8 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
       ->setRevisionable(FALSE)
       ->setSetting('target_type', 'user')
       ->setSetting('handler', 'default')
-      ->setDisplayOptions('view', [
-        'label' => 'hidden',
-        'type' => 'author',
-        'weight' => 0,
-      ])
-      ->setDisplayOptions('form', [
-        'type' => 'entity_reference_autocomplete',
-        'weight' => 5,
-        'settings' => [
-          'match_operator' => 'CONTAINS',
-          'size' => '60',
-          'autocomplete_type' => 'tags',
-          'placeholder' => '',
-        ],
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_leader'] = BaseFieldDefinition::create('entity_reference')
       ->setLabel(t('Leader'))
@@ -312,95 +311,101 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
       ->setRevisionable(FALSE)
       ->setSetting('target_type', 'user')
       ->setSetting('handler', 'default')
-      ->setDisplayOptions('view', [
-        'label' => 'hidden',
-        'type' => 'author',
-        'weight' => 0,
-      ])
-      ->setDisplayOptions('form', [
-        'type' => 'entity_reference_autocomplete',
-        'weight' => 5,
-        'settings' => [
-          'match_operator' => 'CONTAINS',
-          'size' => '60',
-          'autocomplete_type' => 'tags',
-          'placeholder' => '',
-        ],
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['contributor_proposed'] = BaseFieldDefinition::create('timestamp')
       ->setLabel(t('Proposed'))
       ->setDescription(t('The time that the entity was created.'))
       ->setDisplayConfigurable('form', FALSE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['contributor_approved'] = BaseFieldDefinition::create('timestamp')
       ->setLabel(t('Approved'))
       ->setDescription(t('The time that the entity was created.'))
       ->setDisplayConfigurable('form', FALSE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_proposed'] = BaseFieldDefinition::create('timestamp')
       ->setLabel(t('Proposed'))
       ->setDescription(t('The time that the entity was created.'))
       ->setDisplayConfigurable('form', FALSE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_approved'] = BaseFieldDefinition::create('timestamp')
       ->setLabel(t('Approved'))
       ->setDescription(t('The time that the entity was created.'))
       ->setDisplayConfigurable('form', FALSE)
-      ->setDisplayConfigurable('view', TRUE);
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['candidate_correct'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Correct candidate competencies'))
       ->setDescription(t('The number of correct candidate items.'))
-      ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRequired(FALSE)
+      ->setRevisionable(FALSE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['candidate_total'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Total candidate competencies'))
       ->setDescription(t('The total candidate items.'))
-      ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRequired(FALSE)
+      ->setRevisionable(FALSE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['contributor_correct'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Correct contributor competencies'))
       ->setDescription(t('The number of correct contributor items.'))
-      ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRequired(FALSE)
+      ->setRevisionable(FALSE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['contributor_total'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Total contributor competencies'))
       ->setDescription(t('The total contributor items.'))
       ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRevisionable(TRUE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_correct'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Correct publisher competencies'))
       ->setDescription(t('The number of correct publisher items.'))
       ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRevisionable(TRUE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['publisher_total'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Total publisher competencies'))
       ->setDescription(t('The total publisher items.'))
       ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRevisionable(TRUE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', FALSE);
 
     $fields['correct'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Correct competencies'))
       ->setDescription(t('The number of correct items.'))
       ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRevisionable(TRUE)
+      ->setDisplayConfigurable('view', TRUE);
 
     $fields['total'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Total competencies'))
       ->setDescription(t('The total competencies items.'))
       ->setRequired(TRUE)
-      ->setRevisionable(TRUE);
+      ->setRevisionable(TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['completed'] = BaseFieldDefinition::create('timestamp')
+      ->setLabel(t('Completed'))
+      ->setDescription(t('The time that correct = total.'))
+      ->setRevisionable(FALSE)
+      ->setDisplayConfigurable('form', FALSE)
+      ->setDisplayConfigurable('view', TRUE);
 
     $fields['created'] = BaseFieldDefinition::create('created')
       ->setLabel(t('Created'))
@@ -442,22 +447,46 @@ class KnowledgeCompetency extends ContentEntityBase implements EntityChangedInte
   /**
    * {@inheritdoc}
    */
-  public function isPendingContributor(): bool {
-    return !$this->isPendingPublisher() && $this->contributor_coach->target_id != NULL && $this->contributor_leader->target_id == NULL;
-  }
+  public function isPending(): ?string {
+    $roles = $this->get('roles');
+    foreach ($roles as $role) {
+      if ($role->isPending()) {
+        return $role->role;
+      }
+    }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function isPendingPublisher(): bool {
-    return $this->publisher_coach->target_id != NULL && $this->publisher_leader->target_id == NULL;
+    return FALSE;
   }
 
   /**
-   * {@inheritdoc}
+   * Set legacy fields.
    */
-  public function isPending(): bool {
-    return $this->isPendingContributor() || $this->isPendingPublisher();
+  private function setLegacyFields() {
+    $legacy_roles = [
+      'knowledge_candidate',
+      'knowledge_contributor',
+      'knowledge_publisher',
+    ];
+    $roles = $this->get('roles');
+    foreach ($roles as $role) {
+      $name = $role->role;
+      if (!in_array($name, $legacy_roles)) {
+        continue;
+      }
+      $name = str_replace('knowledge_', '', $name);
+
+      $this->set($name . '_correct', $role->correct);
+      $this->set($name . '_total', $role->total);
+
+      if ($name == 'candidate') {
+        continue;
+      }
+
+      $this->set($name . '_proposed', $role->proposed);
+      $this->set($name . '_approved', $role->approved);
+      $this->set($name . '_coach', $role->proposer);
+      $this->set($name . '_leader', $role->approver);
+    }
   }
 
 }
diff --git a/src/Entity/KnowledgeCompetencyViewsData.php b/src/Entity/CompetencyViewsData.php
similarity index 85%
rename from src/Entity/KnowledgeCompetencyViewsData.php
rename to src/Entity/CompetencyViewsData.php
index 3ae535ae6f66fa2d732d0358e6309f88a97f7439..1b487db03c26f61e3c826b948f69024a62860059 100644
--- a/src/Entity/KnowledgeCompetencyViewsData.php
+++ b/src/Entity/CompetencyViewsData.php
@@ -7,7 +7,7 @@ use Drupal\views\EntityViewsData;
 /**
  * Provides Views data for Competency entities.
  */
-class KnowledgeCompetencyViewsData extends EntityViewsData {
+class CompetencyViewsData extends EntityViewsData {
 
   /**
    * {@inheritdoc}
diff --git a/src/Form/CompetencyApproveForm.php b/src/Form/CompetencyApproveForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..675c6046e958b7495a72829d61e32fbe9b74c04c
--- /dev/null
+++ b/src/Form/CompetencyApproveForm.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace Drupal\knowledge\Form;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Url;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the knowledge delete confirmation form.
+ *
+ * @internal
+ */
+class CompetencyApproveForm extends ConfirmFormBase {
+
+  /**
+   * The knowledge competency.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyInterface
+   */
+  protected $competency;
+
+  /**
+   * The user to be promoted.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $learner;
+
+  /**
+   * The role to ne promoted to.
+   *
+   * @var \Drupal\user\RoleInterface
+   */
+  protected $role;
+
+  /**
+   * The current user account.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $account;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The competency storage service.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $competencyStorage;
+
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competencyService;
+
+  /**
+   * The user role storage service.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $userRoleStorage;
+
+  /**
+   * Constructs a new CompetencyApproveForm object.
+   *
+   * @param \Drupal\Core\Session\AccountProxyInterface $account
+   *   The current user account.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $competency_storage
+   *   The competency storage service.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $user_role_storage
+   *   The user role storage service.
+   * @param \Drupal\knowledge\KnowledgeCompetencyServiceInterface $competency_service
+   *   The competency service.
+   */
+  public function __construct(
+    AccountProxyInterface $account,
+    TimeInterface $time,
+    EntityStorageInterface $competency_storage,
+    EntityStorageInterface $user_role_storage,
+    KnowledgeCompetencyServiceInterface $competency_service,
+  ) {
+    $this->account = $account;
+    $this->time = $time;
+    $this->competencyStorage = $competency_storage;
+    $this->userRoleStorage = $user_role_storage;
+    $this->competencyService = $competency_service;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('current_user'),
+      $container->get('datetime.time'),
+      $container->get('entity_type.manager')->getStorage('knowledge_competency'),
+      $container->get('entity_type.manager')->getStorage('user_role'),
+      $container->get('knowledge.competency'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() : string {
+    return "confirm_knowledge_competency_approve_form";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $user = '') {
+    $competency = $this->competencyService->getUserCompetency($user->id());
+    $this->setCompetency($competency);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Promote');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return Url::fromRoute('entity.user.knowledge_competency', [
+      'user' => $this->competency->getOwnerId(),
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRedirectUrl() {
+    return $this->getCancelUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('Do you want to promote the user?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+
+    $username = $this->learner ? $this->learner->label() : '<unknown>';
+    $role = $this->role ? $this->role->label() : '<unknown>';
+
+    $p = [
+      '@username' => $username,
+      '@role' => $role,
+    ];
+    return $this->t('Do you want to promote @username to a @role?', $p);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $competency = $this->competency;
+    $learner = $this->learner;
+    $role = $this->role->id();
+
+    if (!$this->competencyService->hasRoleOrBetter($role, $learner->getRoles())) {
+      $learner->addRole($role);
+      $learner->save();
+      $this->messenger()->addMessage($this->t('@user was promoted to @role.', [
+        '@role' => $this->role->label(),
+        '@user' => $learner->getDisplayName(),
+      ]));
+    }
+
+    $pending_role = $competency->isPending();
+    if ($pending_role == FALSE) {
+      return;
+    }
+    foreach ($competency->get('roles') as $role) {
+      if ($role->role == $pending_role) {
+        $role->approved = $this->time->getRequestTime();
+        $role->approver = $this->account->id();
+      }
+    }
+    $competency->save();
+
+    $form_state->setRedirect('entity.user.knowledge_competency', [
+      'user' => $this->competency->getOwnerId(),
+    ]);
+  }
+
+  /**
+   * Sets the competency pending approval.
+   */
+  public function setCompetency($competency) {
+    $this->competency = $competency;
+    $this->learner = $competency->getOwner();
+
+    $role_id = $competency->isPending();
+    if (!$role_id) {
+      return;
+    }
+
+    $promotion_role = $this->competencyService->getPromotionRole($role_id);
+    if (!$promotion_role) {
+      return;
+    }
+    $this->role = $this->userRoleStorage->load($promotion_role);
+  }
+
+}
diff --git a/src/Form/CompetencyDeleteForm.php b/src/Form/CompetencyDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..b60b7bf4e903d6dfc9e83c3427c132bbe0a71f29
--- /dev/null
+++ b/src/Form/CompetencyDeleteForm.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\knowledge\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+use Drupal\Core\Url;
+
+/**
+ * Provides a form for deleting Competency entities.
+ *
+ * @ingroup knowledge
+ */
+class CompetencyDeleteForm extends ContentEntityDeleteForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRedirectUrl() {
+    $entity = $this->getEntity();
+    $owner = $entity->getOwner();
+
+    return Url::fromRoute('entity.user.knowledge_competency', ['user' => $owner->id()]);
+
+  }
+
+}
diff --git a/src/Form/CompetencyForm.php b/src/Form/CompetencyForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..80a003ef5dc5beb8aee54641aa0ca57dcd16b8a1
--- /dev/null
+++ b/src/Form/CompetencyForm.php
@@ -0,0 +1,257 @@
+<?php
+
+namespace Drupal\knowledge\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\knowledge_field\Helper\CompetencyField;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form controller for Competency edit forms.
+ *
+ * @ingroup knowledge
+ */
+class CompetencyForm extends ContentEntityForm {
+
+  /**
+   * The current user account.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $account;
+
+  /**
+   * The knowledge settings config.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $settings;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatter
+   */
+  protected $dateFormatter;
+
+  /**
+   * The user role storage service.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $roleStorage;
+
+  /**
+   * The field group info array.
+   *
+   * @var array
+   */
+  protected $groups;
+
+  /**
+   * The Renderer service.
+   *
+   * @var \Drupal\Core\Render\Renderer
+   */
+  protected $render;
+
+  /**
+   * The field definitions for the knowledge competency entity.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface[]
+   */
+  protected $definitions;
+
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competencyService;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    // Instantiates this form class.
+    $instance = parent::create($container);
+    $instance->account = $container->get('current_user');
+    $instance->roleStorage = $container->get('entity_type.manager')->getStorage('user_role');
+    $instance->settings = $container->get('config.factory')->get('knowledge.competency.settings');
+    $instance->competencyService = $container->get('knowledge.competency');
+    $instance->dateFormatter = $container->get('date.formatter');
+    $instance->time = $container->get('datetime.time');
+    $instance->render = $container->get('renderer');
+    $instance->definitions = $container->get('entity_field.manager')->getFieldDefinitions('knowledge_competency', 'knowledge_competency');
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+
+    $this->addRoleTabs($form);
+    $this->buildCompetency($form);
+
+    // In several places within this function, we vary $form on:
+    // - The current user's permissions.
+    // - Whether the current user is authenticated or anonymous.
+    // - The 'user.settings' configuration.
+    // - The knowledge field's definition.
+    $form['#cache']['contexts'][] = 'user';
+    $form['#cache']['contexts'][] = 'user.roles:authenticated';
+
+    $form['user_id']['#type'] = 'hidden';
+
+    $form['#attached']['library'][] = 'knowledge/competency_form';
+
+    return $form;
+  }
+
+  /**
+   * Adds the role tabs.
+   *
+   * @param array $form
+   *   The form.
+   */
+  private function addRoleTabs(array &$form) {
+    $roles_weight = $form['roles']['widget'][0]['#weight'];
+    $form['skills'] = [
+      "#type" => "vertical_tabs",
+      "#weight" => $roles_weight,
+      "#default_tab" => "edit-knowledge-publisher",
+    ];
+    $competency_roles = $this->competencyService->getRoleIds();
+    $roles = $this->roleStorage->loadMultiple($competency_roles);
+    foreach ($competency_roles as $role) {
+      $form[$role] = [
+        '#type' => 'details',
+        '#title' => $roles[$role]?->label(),
+        '#group' => 'skills',
+      ];
+    }
+  }
+
+  /**
+   * Builds the competency.
+   *
+   * @param array $form
+   *   The form.
+   */
+  protected function buildCompetency(array &$form) {
+    $roles = [];
+    $role_fields = CompetencyField::roleFields($this->definitions);
+
+    foreach ($role_fields as $role => $fields) {
+      $roles[$role] = [
+        'correct' => 0,
+        'total' => 0,
+      ];
+      foreach ($fields as $field_name) {
+        $roles[$role]['total'] += 1;
+        if ($this->entity->get($field_name)?->value) {
+          $roles[$role]['correct'] += 1;
+        }
+        $form[$role][$field_name] = &$form[$field_name];
+        unset($form[$field_name]);
+      }
+    }
+
+    $setting = $this->competencyService->get();
+    $default_tab = 'edit-' . str_replace('_', '-', $setting[0]['role']);
+    foreach ($setting as $index => $r) {
+
+      $role = $r['role'];
+      if ($roles[$role]['total'] != $roles[$role]['correct']) {
+        break;
+      }
+
+      $next = $setting[$index + 1] ?? NULL;
+      if (is_null($next)) {
+        break;
+      }
+
+      $next_role = $next['role'];
+      $default_tab = 'edit-' . str_replace('_', '-', $next_role);
+    }
+
+    $form['skills']['#default_tab'] = $default_tab;
+  }
+
+  /**
+   * Sets the default tab.
+   *
+   * @param array $form
+   *   The form.
+   */
+  private function setDefaultTab(&$form) {
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $contributor_proposal = $form_state->getValue('contributor_proposal');
+    $publisher_proposal = $form_state->getValue('publisher_proposal');
+    /** @var \Drupal\knowledge\KnowledgeCompetencyInterface $competency */
+    $competency = $this->entity;
+    if (empty($competency->contributor_proposed->value) && $contributor_proposal) {
+      $competency->contributor_proposed->value = $this->time->getCurrentTime();
+      $competency->contributor_coach->target_id = $this->account->id();
+    }
+    elseif (!empty($competency->contributor_proposed->value) && !$contributor_proposal) {
+      $competency->contributor_proposed->value = NULL;
+      $competency->contributor_coach->target_id = NULL;
+      $competency->contributor_leader->target_id = NULL;
+    }
+    if (empty($competency->publisher_proposed->value) && $publisher_proposal) {
+      $competency->publisher_proposed->value = $this->time->getCurrentTime();
+      $competency->publisher_coach->target_id = $this->account->id();
+    }
+    elseif (!empty($competency->publisher_proposed->value) && !$publisher_proposal) {
+      $competency->publisher_proposed->value = NULL;
+      $competency->publisher_coach->target_id = NULL;
+      $competency->publisher_leader->target_id = NULL;
+    }
+    $roles = $competency->get('roles');
+    foreach ($roles as $index => $role) {
+
+      if ($role->proposer) {
+        $roles[$index]->proposer = $role->proposer->id();
+      }
+      if ($role->approver) {
+        $roles[$index]->approver = $role->approver->id();
+      }
+
+    }
+
+    $status = parent::save($form, $form_state);
+
+    switch ($status) {
+      case SAVED_NEW:
+        $this->messenger()->addMessage($this->t("Created %user's Competency.", [
+          '%user' => $competency->getOwner()->label(),
+        ]));
+        break;
+
+      default:
+        $this->messenger()->addMessage($this->t("Saved %user's Competency.", [
+          '%user' => $competency->getOwner()->label(),
+        ]));
+    }
+
+    return $status;
+  }
+
+}
diff --git a/src/Form/CompetencyRoleForm.php b/src/Form/CompetencyRoleForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..ad4f50f3cdbd9380a8d051324d058d9a5eaede4f
--- /dev/null
+++ b/src/Form/CompetencyRoleForm.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace Drupal\knowledge\Form;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Role for the Competency entity.
+ *
+ * @ingroup knowledge
+ */
+class CompetencyRoleForm extends FormBase {
+
+  /**
+   * The user role storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $roleStorage;
+
+  /**
+   * The knowledge settings config.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $settings;
+
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competency;
+
+  /**
+   * The Competency Role settings.
+   *
+   * @param \Drupal\Core\Config\Config $settings
+   *   The settings.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $role_storage
+   *   The role storage.
+   * @param \Drupal\knowledge\KnowledgeCompetencyServiceInterface $competency
+   *   The competency service.
+   */
+  public function __construct(Config $settings, EntityStorageInterface $role_storage, KnowledgeCompetencyServiceInterface $competency) {
+    $this->settings = $settings;
+    $this->roleStorage = $role_storage;
+    $this->competency = $competency;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory')->getEditable('knowledge.competency.settings'),
+      $container->get('entity_type.manager')->getStorage('user_role'),
+      $container->get('knowledge.competency'),
+    );
+  }
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'knowledge_competency_role_settings';
+  }
+
+  /**
+   * Defines the settings form for Competency entities.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+
+    $all_roles = $this->getRoles(TRUE);
+    $competency = $this->competency->get();
+    $competency_roles = $this->competency->getRoleIds();
+
+    $roles = $this->roleStorage->loadMultiple($competency_roles);
+    $form['roles'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Roles'),
+      '#options' => $all_roles,
+      '#multiple' => TRUE,
+      '#default_value' => $competency_roles,
+    ];
+
+    $form['role_weight'] = [
+      '#type' => 'table',
+      '#title' => $this->t('Fields'),
+      '#header' => [
+        $this->t('Role'),
+        $this->t('Weight'),
+        $this->t('Action'),
+        $this->t('Promote'),
+      ],
+      '#tabledrag' => [[
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'draggable-weight',
+      ],
+      ],
+    ];
+    $weight_delta = round(count($competency_roles) / 2);
+    $promote_options = [
+      '_none' => $this->t('None'),
+      '_self' => $this->t('Promote to this role'),
+      '_next' => $this->t('Promote next role'),
+    ];
+    $promote_options = array_merge($promote_options, $competency_roles);
+    foreach ($competency as $info) {
+      $role = $info['role'];
+      $title = $roles[$role]?->label() ?? $role ?? $this->t('Unknown role');
+      $form['role_weight']['#tabledrag'][] = [
+        'action' => 'match',
+        'relationship' => 'sibling',
+        'group' => 'field-role-select',
+        'subgroup' => 'field-role-' . $role,
+        'hidden' => FALSE,
+      ];
+      $form['role_weight']['#tabledrag'][] = [
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'field-weight',
+        'subgroup' => 'field-weight-' . $role,
+      ];
+
+      $form['role_weight'][$role] = [
+        '#attributes' => [
+          'class' => ['role-title', 'role-title-' . $role, 'draggable'],
+          'no_striping' => TRUE,
+        ],
+      ];
+
+      $form['role_weight'][$role]['title'] = [
+        '#theme_wrappers' => [
+          'container' => [
+            '#attributes' => ['class' => 'role-title__action'],
+          ],
+        ],
+        '#prefix' => $title,
+        '#type' => 'link',
+        '#title' => $this->t('Place block <span class="visually-hidden">in the %region region</span>', ['%region' => $title]),
+        '#wrapper_attributes' => [
+          // 'colspan' => 5,
+        ],
+        '#attributes' => [
+          'class' => ['use-ajax', 'button', 'button--small'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'width' => 880,
+          ]),
+        ],
+      ];
+      $form['role_weight'][$role]['weight'] = [
+        '#type' => 'weight',
+        '#default_value' => $info['weight'] ?? 0,
+        '#delta' => $weight_delta,
+        '#title' => $this->t('Weight for @block block', ['@block' => $title]),
+        '#title_display' => 'invisible',
+        '#attributes' => [
+          'class' => ['draggable-weight', 'field-weight', 'field-weight-' . $role],
+        ],
+      ];
+
+      $form['role_weight'][$role]['action'] = [
+        '#type' => 'select',
+        '#options' => [
+          '_none' => $this->t('Do nothing'),
+          'auto' => $this->t('Promote automatically'),
+          'leader' => $this->t('Promote with leadership approval'),
+        ],
+        '#default_value' => $info['action'] ?? '_none',
+        '#title' => $this->t('Action for @field field', ['@field' => $title]),
+        '#title_display' => 'invisible',
+        '#parents' => ['role_weight', $role, 'action'],
+      ];
+
+      $form['role_weight'][$role]['promote'] = [
+        '#type' => 'select',
+        '#options' => [
+          'self' => $this->t('Promote to this role'),
+          'next' => $this->t('Promote next role'),
+        ],
+        '#default_value' => $info['promote'] ?? '_none',
+        '#title' => $this->t('Promote for @field field', ['@field' => $title]),
+        '#title_display' => 'invisible',
+        '#parents' => ['role_weight', $role, 'promote'],
+        '#states' => [
+          'invisible' => [
+            ':input[name="role_weight[' . $role . '][action]"]' => ['value' => '_none'],
+          ],
+        ],
+      ];
+    }
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+      '#button_type' => 'primary',
+      '#submit' => ['::submitForm'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Get all roles.
+   *
+   * @return array
+   *   The roles.
+   */
+  private function getRoles() {
+    $roles = [];
+    foreach ($this->roleStorage->loadMultiple() as $role) {
+      $roles[$role->id()] = $role->label();
+    }
+    unset($roles['anonymous']);
+    unset($roles['authenticated']);
+    unset($roles['administrator']);
+    unset($roles['knowledge_coach']);
+    unset($roles['knowledge_leader']);
+
+    return $roles;
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+
+    $roles = [];
+    $role_weight = $form_state->getValue('role_weight');
+    foreach ($form_state->getValue('roles') as $role) {
+      $weight = isset($role_weight[$role]['weight']) ? (int) $role_weight[$role]['weight'] : 0;
+      $action = $role_weight[$role]['action'] ?? '_none';
+      $promote = $role_weight[$role]['promote'] ?? '_none';
+      if ($action === '_none') {
+        $promote = '_none';
+      }
+      $roles[] = [
+        'role' => $role,
+        'weight' => $weight,
+        'action' => $action,
+        'promote' => $promote,
+      ];
+    }
+
+    $this->settings
+      ->set('roles', $roles)
+      ->save();
+  }
+
+}
diff --git a/src/Form/CompetencySettingsForm.php b/src/Form/CompetencySettingsForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..466a9a32fe000a28b89bd684965f495afcdd554c
--- /dev/null
+++ b/src/Form/CompetencySettingsForm.php
@@ -0,0 +1,317 @@
+<?php
+
+namespace Drupal\knowledge\Form;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
+use Drupal\knowledge_field\Helper\CompetencyField;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Settings for the KnowledgeCompetency entity.
+ *
+ * @ingroup knowledge
+ */
+class CompetencySettingsForm extends FormBase {
+
+  /**
+   * The user role storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $roleStorage;
+
+  /**
+   * The knowledge settings config.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $settings;
+
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competencyService;
+
+  /**
+   * The Competency settings form.
+   *
+   * @param \Drupal\Core\Config\ImmutableConfig $settings
+   *   The settings.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $role_storage
+   *   The role storage.
+   * @param \Drupal\knowledge\KnowledgeCompetencyServiceInterface $competency_service
+   *   The competency service.
+   */
+  public function __construct(ImmutableConfig $settings, EntityStorageInterface $role_storage, KnowledgeCompetencyServiceInterface $competency_service) {
+    $this->settings = $settings;
+    $this->roleStorage = $role_storage;
+    $this->competencyService = $competency_service;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory')->get('knowledge.competency.settings'),
+      $container->get('entity_type.manager')->getStorage('user_role'),
+      $container->get('knowledge.competency')
+    );
+  }
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'knowledge_competency_settings';
+  }
+
+  /**
+   * Defines the settings form for Competency entities.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+
+    $competency_roles = $this->competencyService->getRoleIds();
+    $role_options = [];
+    $role_options['_none'] = $this->t('No role');
+    foreach ($this->roleStorage->loadMultiple($competency_roles) as $role) {
+      $role_options[$role->id()] = $role->label();
+    }
+
+    $fields = [];
+    $items = $this->getFields();
+    $weight_delta = round(count($items) / 2);
+
+    $placement = FALSE;
+    foreach ($items as $field_id => $field) {
+
+      $fields[$field['role']][$field_id] = [
+        'label' => $field['label'],
+        'entity_id' => $field_id,
+        'weight' => $field['weight'],
+        'entity' => $field,
+        'status' => 1,
+      ];
+    }
+
+    $form['fields'] = [
+      '#type' => 'table',
+      '#title' => $this->t('Fields'),
+      '#header' => [
+        $this->t('Field'),
+        $this->t('Role'),
+        $this->t('Weight'),
+      ],
+      '#tabledrag' => [[
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'draggable-weight',
+      ],
+      ],
+      '#empty' => $this->t('No items.'),
+      '#tableselect' => FALSE,
+    ];
+    $weight = 0;
+    $competency_roles[] = '_none';
+
+    foreach ($competency_roles as $role) {
+
+      $title = $role_options[$role];
+      $form['fields']['#tabledrag'][] = [
+        'action' => 'match',
+        'relationship' => 'sibling',
+        'group' => 'field-role-select',
+        'subgroup' => 'field-role-' . $role,
+        'hidden' => FALSE,
+      ];
+      $form['fields']['#tabledrag'][] = [
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'field-weight',
+        'subgroup' => 'field-weight-' . $role,
+      ];
+
+      $form['fields']['role-' . $role] = [
+        '#attributes' => [
+          'class' => ['role-title', 'role-title-' . $role],
+          'no_striping' => TRUE,
+        ],
+      ];
+      $form['fields']['role-' . $role]['title'] = [
+        '#theme_wrappers' => [
+          'container' => [
+            '#attributes' => ['class' => 'role-title__action'],
+          ],
+        ],
+        '#prefix' => $title,
+        '#type' => 'link',
+        '#title' => $this->t('Place block <span class="visually-hidden">in the %region region</span>', ['%region' => $title]),
+        '#wrapper_attributes' => [
+          'colspan' => 5,
+        ],
+        '#attributes' => [
+          'class' => ['use-ajax', 'button', 'button--small'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'width' => 880,
+          ]),
+        ],
+      ];
+
+      $form['fields']['role-' . $role . '-message'] = [
+        '#attributes' => [
+          'class' => [
+            'role-message',
+            'role-' . $role . '-message',
+            empty($fields[$role]) ? 'role-empty' : 'role-populated',
+          ],
+        ],
+      ];
+      $form['fields']['role-' . $role . '-message']['message'] = [
+        '#markup' => '<em>' . $this->t('No competencies in this role') . '</em>',
+        '#wrapper_attributes' => [
+          'colspan' => 5,
+        ],
+      ];
+
+      if (isset($fields[$role])) {
+        foreach ($fields[$role] as $info) {
+          $field_id = $info['entity_id'];
+
+          $form['fields'][$field_id] = [
+            '#attributes' => [
+              'class' => ['draggable'],
+            ],
+          ];
+          $form['fields'][$field_id]['#attributes']['class'][] = $info['status'] ? 'fields-enabled' : 'fields-disabled';
+          if ($placement && $placement == Html::getClass($field_id)) {
+            $form['fields'][$field_id]['#attributes']['class'][] = 'color-success';
+            $form['fields'][$field_id]['#attributes']['class'][] = 'js-fields-placed';
+          }
+          $form['fields'][$field_id]['info'] = [
+            '#wrapper_attributes' => [
+              'class' => ['field'],
+            ],
+          ];
+          // Ensure that the label is always rendered as plain text. Render
+          // array #plain_text key is essentially treated same as @ placeholder
+          // in translatable markup.
+          if ($info['status']) {
+            $form['fields'][$field_id]['info']['#plain_text'] = $info['label'];
+          }
+          else {
+            $form['fields'][$field_id]['info']['#markup'] = $this->t('@label (disabled)', ['@label' => $info['label']]);
+          }
+
+          $form['fields'][$field_id]['role-theme']['role'] = [
+            '#type' => 'select',
+            '#default_value' => $role,
+            '#required' => TRUE,
+            '#title' => $this->t('Role for @field field', ['@field' => $info['label']]),
+            '#title_display' => 'invisible',
+            '#options' => $role_options,
+            '#attributes' => [
+              'class' => ['field-role-select', 'field-role-' . $role],
+            ],
+            '#parents' => ['fields', $field_id, 'role'],
+          ];
+
+          $form['fields'][$field_id]['weight'] = [
+            '#type' => 'weight',
+            '#default_value' => $info['weight'],
+            '#delta' => $weight_delta,
+            '#title' => $this->t('Weight for @block block', ['@block' => $info['label']]),
+            '#title_display' => 'invisible',
+            '#attributes' => [
+              'class' => ['draggable-weight', 'field-weight', 'field-weight-' . $role],
+            ],
+          ];
+
+        }
+      }
+    }
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+      '#button_type' => 'primary',
+      '#submit' => ['::submitForm'],
+    ];
+
+    $form['#attached']['library'][] = 'core/drupal.tableheader';
+    $form['#attached']['library'][] = 'knowledge/competency.admin';
+
+    return $form;
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty implementation of the abstract submit class.
+    $fields = $form_state->getValue('fields');
+
+    $fields_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('knowledge_competency', 'knowledge_competency');
+    foreach ($fields as $field_id => $field) {
+      $role = $field['role'];
+      $fields_definitions[$field_id]->setThirdPartySetting('knowledge', 'competency_role', $role);
+      $fields_definitions[$field_id]->save();
+    }
+
+  }
+
+  /**
+   * Get all fields.
+   */
+  protected function getFields() {
+    $form = \Drupal::service('entity_display.repository')->getFormDisplay('knowledge_competency', 'knowledge_competency', 'default');
+
+    $fields_definitions = $form->get('fieldDefinitions');
+    $content = $form->get('content');
+    $fields = [];
+    $role_fields = CompetencyField::roleFields($fields_definitions);
+    foreach ($role_fields as $role => $field_names) {
+      foreach ($field_names as $field_name) {
+        if (!array_key_exists($field_name, $content)) {
+          continue;
+        }
+
+        $fields[$field_name] = [
+          'label' => $fields_definitions[$field_name]->getLabel(),
+          'weight' => $content[$field_name]['weight'],
+          'role' => $role,
+        ];
+      }
+    }
+
+    return $fields;
+  }
+
+}
diff --git a/src/Form/GovernanceForm.php b/src/Form/GovernanceForm.php
index e139dd205bda2d78894e8e7a40ba333fec687f6d..f0c9cbd425487ff2333a1119e85207559ba78280 100644
--- a/src/Form/GovernanceForm.php
+++ b/src/Form/GovernanceForm.php
@@ -66,7 +66,11 @@ final class GovernanceForm extends ConfigFormBase {
     $settings = $this->config('knowledge.governance.settings');
     $roles = $this->roleStorage->loadMultiple();
     $options = [];
+    $skip_roles = ['anonymous', 'authenticated'];
     foreach ($roles as $machine_name => $role) {
+      if (in_array($machine_name, $skip_roles)) {
+        continue;
+      }
       $options[$machine_name] = $role->label();
     }
     $form['roles'] = [
diff --git a/src/Form/KnowledgeCompetencyApproveForm.php b/src/Form/KnowledgeCompetencyApproveForm.php
deleted file mode 100644
index fadaf9a9bc7e9c2a05d0b08ed1bd22d59ee1bb5e..0000000000000000000000000000000000000000
--- a/src/Form/KnowledgeCompetencyApproveForm.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-
-namespace Drupal\knowledge\Form;
-
-use Drupal\Core\Form\ConfirmFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\user\Entity\Role;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Provides the knowledge delete confirmation form.
- *
- * @internal
- */
-class KnowledgeCompetencyApproveForm extends ConfirmFormBase {
-
-  /**
-   * The knowledge competency.
-   *
-   * @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
-   */
-  protected $competency;
-
-  /**
-   * The user to be promoted.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $learner;
-
-  /**
-   * The role to ne promoted to.
-   *
-   * @var \Drupal\user\RoleInterface
-   */
-  protected $role;
-
-  /**
-   * The current user account.
-   *
-   * @var \Drupal\Core\Session\AccountProxyInterface
-   */
-  protected $account;
-
-  /**
-   * The time service.
-   *
-   * @var \Drupal\Component\Datetime\TimeInterface
-   */
-  protected $time;
-
-  /**
-   * The competency storage service.
-   *
-   * @var \Drupal\Core\Entity\EntityStorageInterface
-   */
-  protected $competencyStorage;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    // Instantiates this form class.
-    $instance = parent::create($container);
-    $instance->account = $container->get('current_user');
-    $instance->time = $container->get('datetime.time');
-    $instance->competencyStorage = $container->get('entity_type.manager')
-      ->getStorage('knowledge_competency');
-    return $instance;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() : string {
-    return "confirm_knowledge_competency_approve_form";
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, $user = '') {
-    $competency = $this->getCompetency($user);
-    $this->setCompetency($competency);
-    $form = parent::buildForm($form, $form_state);
-
-    return $form;
-  }
-
-  /**
-   * Get the user's competency.
-   */
-  protected function getCompetency($user) {
-    $query = $this->competencyStorage->getQuery();
-    $result = $query
-      ->condition('user_id', $user)
-      ->accessCheck(FALSE)
-      ->execute();
-    $competency = NULL;
-    if (count($result) == 1) {
-      $competency = $this->competencyStorage->load(current($result));
-    }
-    if (count($result) == 0) {
-      $competency = $this->competencyStorage->create([
-        'user_id' => $user,
-      ]);
-    }
-
-    return $competency;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConfirmText() {
-    return $this->t('Promote');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelUrl() {
-    // Point to the entity of which this knowledge is a reply.
-    return $this->competency->toUrl();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getRedirectUrl() {
-    return $this->getCancelUrl();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDescription() {
-    return $this->t('Do you want to promote the user?');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getQuestion() {
-    $username = $this->learner ? $this->learner->label() : '<unknown>';
-    $role = $this->role ? $this->role->label() : '<unknown>';
-    $p = [
-      '@username' => $username,
-      '@role' => $role,
-    ];
-    return $this->t('Do you want to promote @username to a @role?', $p);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $competency = $this->competency;
-    $learner = $this->learner;
-    $role = $this->role->id();
-
-    if ($learner->hasRole('knowledge_candidate')) {
-      $learner->removeRole('knowledge_candidate');
-    }
-    if ($role == 'knowledge_publisher' && $learner->hasRole('knowledge_contributor')) {
-      $learner->removeRole('knowledge_contributor');
-    }
-
-    $learner->addRole($role);
-    $learner->save();
-    if ($competency->isPendingPublisher()) {
-      $competency->publisher_approved->value = $this->time->getCurrentTime();
-      $competency->publisher_leader->target_id = $this->account->id();
-    }
-    elseif ($competency->isPendingContributor()) {
-      $competency->contributor_approved->value = $this->time->getCurrentTime();
-      $competency->contributor_leader->target_id = $this->account->id();
-    }
-    $competency->save();
-
-    $form_state->setRedirect('entity.knowledge_competency.canonical', ['knowledge_competency' => $this->competency->id()]);
-  }
-
-  /**
-   * Sets the competency pending approval.
-   */
-  public function setCompetency($competency) {
-    $this->competency = $competency;
-    $this->learner = $competency->getOwner();
-    if ($competency->isPendingContributor()) {
-      $this->role = Role::load('knowledge_contributor');
-    }
-    elseif ($competency->isPendingPublisher()) {
-      $this->role = Role::load('knowledge_publisher');
-    }
-
-  }
-
-}
diff --git a/src/Form/KnowledgeCompetencyDeleteForm.php b/src/Form/KnowledgeCompetencyDeleteForm.php
deleted file mode 100644
index ba9852e5d78aa48b00252feb67f0769d16432b20..0000000000000000000000000000000000000000
--- a/src/Form/KnowledgeCompetencyDeleteForm.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Drupal\knowledge\Form;
-
-use Drupal\Core\Entity\ContentEntityDeleteForm;
-
-/**
- * Provides a form for deleting Competency entities.
- *
- * @ingroup knowledge
- */
-class KnowledgeCompetencyDeleteForm extends ContentEntityDeleteForm {
-
-
-}
diff --git a/src/Form/KnowledgeCompetencyForm.php b/src/Form/KnowledgeCompetencyForm.php
deleted file mode 100644
index f120760cc7080e97fe3268ece2136f11b23f38b0..0000000000000000000000000000000000000000
--- a/src/Form/KnowledgeCompetencyForm.php
+++ /dev/null
@@ -1,349 +0,0 @@
-<?php
-
-namespace Drupal\knowledge\Form;
-
-use Drupal\Core\Entity\ContentEntityForm;
-use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Form controller for Competency edit forms.
- *
- * @ingroup knowledge
- */
-class KnowledgeCompetencyForm extends ContentEntityForm {
-
-  /**
-   * The current user account.
-   *
-   * @var \Drupal\Core\Session\AccountProxyInterface
-   */
-  protected $account;
-
-  /**
-   * The knowledge settings config.
-   *
-   * @var \Drupal\Core\Config\Config
-   */
-  protected $config;
-
-  /**
-   * The database connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $database;
-
-  /**
-   * The date formatter service.
-   *
-   * @var \Drupal\Core\Datetime\DateFormatter
-   */
-  protected $dateFormatter;
-
-  /**
-   * The user role storage service.
-   *
-   * @var \Drupal\Core\Entity\EntityStorageInterface
-   */
-  protected $roleStorage;
-
-  /**
-   * The field group info array.
-   *
-   * @var array
-   */
-  protected $groups;
-
-  /**
-   * The Renderer service.
-   *
-   * @var \Drupal\Core\Render\Renderer
-   */
-  protected $render;
-
-  /**
-   * The block manager service.
-   *
-   * @var \Drupal\Core\Block\BlockManagerInterface
-   */
-  protected $blockManager;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    // Instantiates this form class.
-    $instance = parent::create($container);
-    $instance->account = $container->get('current_user');
-    $instance->roleStorage = $container->get('entity_type.manager')->getStorage('user_role');
-    $instance->config = $container->get('config.factory')->get('knowledge.settings');
-    $instance->dateFormatter = $container->get('date.formatter');
-    $instance->time = $container->get('datetime.time');
-    $instance->groups = field_group_info_groups('knowledge_competency', 'knowledge_competency', 'form', 'default');
-    $instance->render = $container->get('renderer');
-    $instance->blockManager = $container->get('plugin.manager.block');
-    return $instance;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
-    // In several places within this function, we vary $form on:
-    // - The current user's permissions.
-    // - Whether the current user is authenticated or anonymous.
-    // - The 'user.settings' configuration.
-    // - The knowledge field's definition.
-    $form['#cache']['contexts'][] = 'user';
-    $form['#cache']['contexts'][] = 'user.roles:authenticated';
-
-    unset($form['revision_information']);
-    unset($form['revision']);
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $competency */
-    $competency = $this->entity;
-    $user = $competency->getOwner();
-    $roles = $user->getRoles();
-
-    $is_publisher = in_array('knowledge_publisher', $roles);
-    $is_contributor = in_array('knowledge_contributor', $roles) && !$is_publisher;
-    $is_candidate = in_array('knowledge_candidate', $roles) && !$is_contributor && !$is_publisher;
-
-    if (!($is_candidate || $is_contributor || $is_publisher)) {
-      return ['#markup' => $this->t('@username is not a knowledge worker.', ['@username' => $user->toLink()->toString()])];
-    }
-
-    $form['user_id']['#type'] = 'hidden';
-
-    $candidate_option = $this->config->get('competency.candidate');
-    $has_candidate_option = $candidate_option != '_none';
-    $contributor_option = $this->config->get('competency.contributor');
-    $has_contributor_option = $contributor_option != '_none';
-    $publisher_option = $this->config->get('competency.publisher');
-    $has_publisher_option = $publisher_option != '_none';
-    $all_option = $this->config->get('competency.all');
-    $has_all_option = $all_option != '_none';
-
-    if ($has_candidate_option) {
-      if ($candidate_option == 'contributor' && $is_candidate) {
-        $this->addPromoteContributorField($form);
-        $this->addCandidateChecks($form, $candidate_option);
-      }
-      elseif ($candidate_option == 'publisher' && $is_contributor) {
-        $this->addPromotePublisherField($form);
-        $this->addCandidateChecks($form, $candidate_option);
-      }
-    }
-
-    if ($has_contributor_option) {
-      if ($contributor_option == 'contributor' && $is_candidate) {
-        $this->addPromoteContributorField($form);
-        $this->addContributorChecks($form, $contributor_option);
-      }
-      elseif ($contributor_option == 'publisher' && $is_contributor) {
-        $this->addPromotePublisherField($form);
-        $this->addContributorChecks($form, $contributor_option);
-      }
-    }
-
-    if ($has_publisher_option) {
-      if ($publisher_option == 'contributor' && $is_candidate) {
-        $this->addPromoteContributorField($form);
-        $this->addPublisherChecks($form, $publisher_option);
-      }
-      elseif ($publisher_option == 'publisher' && $is_contributor) {
-        $this->addPromotePublisherField($form);
-        $this->addPublisherChecks($form, $publisher_option);
-      }
-    }
-
-    if ($has_all_option) {
-      if ($all_option == 'contributor' && $is_candidate) {
-        $this->addPromoteContributorField($form);
-        $this->addAllChecks($form, $all_option);
-      }
-      elseif ($all_option == 'publisher' && $is_contributor) {
-        $this->addPromotePublisherField($form);
-        $this->addAllChecks($form, $all_option);
-      }
-    }
-
-    $form['#attached']['library'][] = 'knowledge/competency_form';
-
-    return $form;
-  }
-
-  /**
-   * Renders a block plugin as markup.
-   */
-  protected function renderPluginBlock($plugin_id, $config = []) {
-    $plugin_block = $this->blockManager->createInstance($plugin_id, $config);
-    // Some blocks might implement access check.
-    $access_result = $plugin_block->access($this->account);
-
-    // Return empty render array if user doesn't have access.
-    // $access_result can be boolean or an AccessResult class.
-    if (is_object($access_result)
-      && $access_result->isForbidden()
-      || is_bool($access_result) && !$access_result) {
-      return [];
-    }
-
-    $render = $plugin_block->build();
-
-    $this->render->addCacheableDependency($render, $plugin_block);
-
-    return $render;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function save(array $form, FormStateInterface $form_state) {
-    $contributor_proposal = $form_state->getValue('contributor_proposal');
-    $publisher_proposal = $form_state->getValue('publisher_proposal');
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $competency */
-    $competency = $this->entity;
-    if (empty($competency->contributor_proposed->value) && $contributor_proposal) {
-      $competency->contributor_proposed->value = $this->time->getCurrentTime();
-      $competency->contributor_coach->target_id = $this->account->id();
-    }
-    elseif (!empty($competency->contributor_proposed->value) && !$contributor_proposal) {
-      $competency->contributor_proposed->value = NULL;
-      $competency->contributor_coach->target_id = NULL;
-      $competency->contributor_leader->target_id = NULL;
-    }
-    if (empty($competency->publisher_proposed->value) && $publisher_proposal) {
-      $competency->publisher_proposed->value = $this->time->getCurrentTime();
-      $competency->publisher_coach->target_id = $this->account->id();
-    }
-    elseif (!empty($competency->publisher_proposed->value) && !$publisher_proposal) {
-      $competency->publisher_proposed->value = NULL;
-      $competency->publisher_coach->target_id = NULL;
-      $competency->publisher_leader->target_id = NULL;
-    }
-    $status = parent::save($form, $form_state);
-
-    switch ($status) {
-      case SAVED_NEW:
-        $this->messenger()->addMessage($this->t('Created the %label Competency.', [
-          '%label' => $competency->label(),
-        ]));
-        break;
-
-      default:
-        $this->messenger()->addMessage($this->t('Saved the %label Competency.', [
-          '%label' => $competency->label(),
-        ]));
-    }
-
-    return $status;
-  }
-
-  /**
-   * Adds the contributor proposal field.
-   */
-  protected function addPromoteContributorField(array &$form) {
-    $contributor = $this->roleStorage->load('knowledge_contributor');
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $competency */
-    $competency = $this->entity;
-    $is_proposed = !empty($competency->contributor_proposed->value);
-    if ($is_proposed) {
-      $contributor_coach = $competency->contributor_coach->entity->label();
-      $date = $this->dateFormatter->format($competency->contributor_proposed->value, 'long');
-      $description = $this->t('@username proposed on @date', [
-        '@username' => $contributor_coach,
-        '@date' => $date,
-      ]);
-    }
-    else {
-      $description = $this->t('Propose user be promoted to a @contributor.',
-        [
-          '@contributor' => $contributor->label(),
-        ]);
-    }
-    $form['contributor_proposal'] = [
-      '#type' => 'checkbox',
-      '#title' => $contributor->label(),
-      '#description' => $description,
-      '#group' => 'second',
-      '#default_value' => $is_proposed,
-      '#weight' => 0,
-    ];
-  }
-
-  /**
-   * Adds the publisher proposal field.
-   */
-  protected function addPromotePublisherField(array &$form) {
-    $publisher = $this->roleStorage->load('knowledge_publisher');
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $competency */
-    $competency = $this->entity;
-    $is_proposed = !empty($competency->publisher_proposed->value);
-    if ($is_proposed) {
-      $publisher_coach = $competency->publisher_coach->entity->label();
-      $date = $this->dateFormatter->format($competency->publisher_proposed->value, 'long');
-      $description = $this->t('@username proposed on @date', [
-        '@username' => $publisher_coach,
-        '@date' => $date,
-      ]);
-    }
-    else {
-      $description = $this->t('Propose user be promoted to a @publisher.',
-        [
-          '@publisher' => $publisher->label(),
-        ]);
-    }
-    $form['publisher_proposal'] = [
-      '#type' => 'checkbox',
-      '#title' => $publisher->label(),
-      '#description' => $description,
-      '#group' => 'second',
-      '#default_value' => $is_proposed,
-    ];
-
-  }
-
-  /**
-   * Adds state checks to a field based on a role.
-   */
-  protected function addChecks(array &$form, string $role_name, string $group_name) {
-    foreach ($this->groups[$group_name]->children as $field_name) {
-      $form[$role_name . '_proposal']['#states']['visible'][':input[name="' . $field_name . '[value]"]'] = [
-        'checked' => TRUE,
-      ];
-    }
-  }
-
-  /**
-   * Adds candidate state checks to a field.
-   */
-  protected function addCandidateChecks(array &$form, string $role_name) {
-    $this->addChecks($form, $role_name, 'group_candidate');
-  }
-
-  /**
-   * Adds contributor state checks to a field.
-   */
-  protected function addContributorChecks(array &$form, string $role_name) {
-    $this->addChecks($form, $role_name, 'group_contributor');
-  }
-
-  /**
-   * Adds publisher state checks to a field.
-   */
-  protected function addPublisherChecks(array &$form, string $role_name) {
-    $this->addChecks($form, $role_name, 'group_publisher');
-  }
-
-  /**
-   * Adds all state checks to a field.
-   */
-  protected function addAllChecks(array &$form, string $role_name) {
-    $this->addCandidateChecks($form, $role_name);
-    $this->addContributorChecks($form, $role_name);
-    $this->addPublisherChecks($form, $role_name);
-  }
-
-}
diff --git a/src/Form/KnowledgeCompetencyRevisionDeleteForm.php b/src/Form/KnowledgeCompetencyRevisionDeleteForm.php
index f241044c105567e189db17c437057beb28df0a0f..e72d3e5042e643f94d1c4ec4fca89edb4ce03c86 100644
--- a/src/Form/KnowledgeCompetencyRevisionDeleteForm.php
+++ b/src/Form/KnowledgeCompetencyRevisionDeleteForm.php
@@ -25,7 +25,7 @@ class KnowledgeCompetencyRevisionDeleteForm extends ConfirmFormBase {
   /**
    * The Competency revision.
    *
-   * @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @var \Drupal\knowledge\KnowledgeCompetencyInterface
    */
   protected $revision;
 
diff --git a/src/Form/KnowledgeCompetencyRevisionRevertForm.php b/src/Form/KnowledgeCompetencyRevisionRevertForm.php
index e4dc79100100457ea40608810da80fb04499e6ff..a500821267e1dc2d39d7364fc61a9dad0fe9ff1f 100644
--- a/src/Form/KnowledgeCompetencyRevisionRevertForm.php
+++ b/src/Form/KnowledgeCompetencyRevisionRevertForm.php
@@ -5,7 +5,7 @@ namespace Drupal\knowledge\Form;
 use Drupal\Core\Form\ConfirmFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
-use Drupal\knowledge\Entity\KnowledgeCompetencyInterface;
+use Drupal\knowledge\KnowledgeCompetencyInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -18,7 +18,7 @@ class KnowledgeCompetencyRevisionRevertForm extends ConfirmFormBase {
   /**
    * The Competency revision.
    *
-   * @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @var \Drupal\knowledge\KnowledgeCompetencyInterface
    */
   protected $revision;
 
@@ -132,12 +132,12 @@ class KnowledgeCompetencyRevisionRevertForm extends ConfirmFormBase {
   /**
    * Prepares a revision to be reverted.
    *
-   * @param \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $revision
+   * @param \Drupal\knowledge\KnowledgeCompetencyInterface $revision
    *   The revision to be reverted.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
    *
-   * @return \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @return \Drupal\knowledge\KnowledgeCompetencyInterface
    *   The prepared revision ready to be stored.
    */
   protected function prepareRevertedRevision(KnowledgeCompetencyInterface $revision, FormStateInterface $form_state) {
diff --git a/src/Form/KnowledgeCompetencySettingsForm.php b/src/Form/KnowledgeCompetencySettingsForm.php
deleted file mode 100644
index a23ff43ce0d27900d61a01861a731272f57f4ed3..0000000000000000000000000000000000000000
--- a/src/Form/KnowledgeCompetencySettingsForm.php
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-
-namespace Drupal\knowledge\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Settings for the KnowledgeCompetency entity.
- *
- * @ingroup knowledge
- */
-class KnowledgeCompetencySettingsForm extends FormBase {
-
-  /**
-   * The user role storage.
-   *
-   * @var \Drupal\Core\Entity\EntityStorageInterface
-   */
-  protected $roleStorage;
-
-  /**
-   * The knowledge settings config.
-   *
-   * @var \Drupal\Core\Config\Config
-   */
-  protected $config;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    // Instantiates this form class.
-    $instance = parent::create($container);
-    $instance->roleStorage = $container->get('entity_type.manager')->getStorage('user_role');
-    $instance->config = $container->get('config.factory')->getEditable('knowledge.settings');
-    return $instance;
-  }
-
-  /**
-   * Returns a unique string identifying the form.
-   *
-   * @return string
-   *   The unique string identifying the form.
-   */
-  public function getFormId() {
-    return 'knowledge_competency_settings';
-  }
-
-  /**
-   * Form submission handler.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    // Empty implementation of the abstract submit class.
-    $this->config
-      ->set('competency.candidate', $form_state->getValue('candidate'))
-      ->set('competency.contributor', $form_state->getValue('contributor'))
-      ->set('competency.publisher', $form_state->getValue('publisher'))
-      ->set('competency.all', $form_state->getValue('all'))
-      ->save();
-  }
-
-  /**
-   * Defines the settings form for Competency entities.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return array
-   *   Form definition array.
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $candidate = $this->roleStorage->load('knowledge_candidate');
-    $contributor = $this->roleStorage->load('knowledge_contributor');
-    $publisher = $this->roleStorage->load('knowledge_publisher');
-    $options = [
-      '_none' => $this->t('Do nothing'),
-      'contributor' => $this->t('Promote to @contributor', [
-        '@contributor' => $contributor->label(),
-      ]),
-      'publisher' => $this->t('Promote to @publisher', [
-        '@publisher' => $publisher->label(),
-      ]),
-    ];
-    $form['candidate'] = [
-      '#title' => $candidate->label(),
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => $this->config->get('competency.candidate'),
-    ];
-    $form['contributor'] = [
-      '#title' => $contributor->label(),
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => $this->config->get('competency.contributor'),
-    ];
-
-    $form['publisher'] = [
-      '#title' => $publisher->label(),
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => $this->config->get('competency.publisher'),
-    ];
-
-    $form['all'] = [
-      '#title' => $this->t('All'),
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => $this->config->get('competency.all'),
-    ];
-
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#button_type' => 'primary',
-      '#submit' => ['::submitForm'],
-    ];
-
-    return $form;
-  }
-
-}
diff --git a/src/Form/MergeNodeForm.php b/src/Form/MergeNodeForm.php
index d2523338dc9a850204d8c591cf7c36ec052ebac0..1cad43091d1a03dd443cb1cd1b29cc2bb3328c45 100644
--- a/src/Form/MergeNodeForm.php
+++ b/src/Form/MergeNodeForm.php
@@ -47,7 +47,7 @@ class MergeNodeForm extends FormBase {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state, NodeInterface $original = NULL, NodeInterface $duplicate = NULL) {
+  public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $original = NULL, ?NodeInterface $duplicate = NULL) {
 
     $form_state->set('original_node', $original);
     $form_state->set('duplicate_node', $duplicate);
diff --git a/src/KnowledgeAccessControlHandler.php b/src/KnowledgeAccessControlHandler.php
index f0c593660f2bec305760d931aa3c9b19cb2a70b2..60aa0af25fcce9392f83fffd1ac033a7dafb1cd2 100644
--- a/src/KnowledgeAccessControlHandler.php
+++ b/src/KnowledgeAccessControlHandler.php
@@ -68,7 +68,7 @@ class KnowledgeAccessControlHandler extends EntityAccessControlHandler {
   /**
    * {@inheritdoc}
    */
-  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
     if ($operation == 'edit') {
       // Only users with the "administer knowledge" permission can edit
       // administrative fields.
diff --git a/src/KnowledgeCompetencyAccessControlHandler.php b/src/KnowledgeCompetencyAccessControlHandler.php
deleted file mode 100644
index 8a9e1dd7228a6638540bf24dbf87cc363c1880c6..0000000000000000000000000000000000000000
--- a/src/KnowledgeCompetencyAccessControlHandler.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-namespace Drupal\knowledge;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityAccessControlHandler;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Access controller for the Competency entity.
- *
- * @see \Drupal\knowledge\Entity\KnowledgeCompetency.
- */
-class KnowledgeCompetencyAccessControlHandler extends EntityAccessControlHandler {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $entity */
-
-    switch ($operation) {
-
-      case 'view':
-
-        if (!$entity->isPublished()) {
-          return AccessResult::allowedIfHasPermission($account, 'view unpublished competency entities');
-        }
-
-        return AccessResult::allowedIfHasPermission($account, 'view published competency entities');
-
-      case 'update':
-
-        return AccessResult::allowedIfHasPermission($account, 'edit competency entities');
-
-      case 'delete':
-
-        return AccessResult::allowedIfHasPermission($account, 'delete competency entities');
-    }
-
-    // Unknown operation, no opinion.
-    return AccessResult::neutral();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    return AccessResult::allowedIfHasPermission($account, 'add competency entities');
-  }
-
-}
diff --git a/src/Entity/KnowledgeCompetencyInterface.php b/src/KnowledgeCompetencyInterface.php
similarity index 79%
rename from src/Entity/KnowledgeCompetencyInterface.php
rename to src/KnowledgeCompetencyInterface.php
index 05fb086c47e850d02e06d6b75418c0d7665d5009..3b9389420dc659593dd2807d898832cdac127c7e 100644
--- a/src/Entity/KnowledgeCompetencyInterface.php
+++ b/src/KnowledgeCompetencyInterface.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\knowledge\Entity;
+namespace Drupal\knowledge;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityChangedInterface;
@@ -32,7 +32,7 @@ interface KnowledgeCompetencyInterface extends ContentEntityInterface, RevisionL
    * @param int $timestamp
    *   The Competency creation timestamp.
    *
-   * @return \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @return \Drupal\knowledge\KnowledgeCompetencyInterface
    *   The called Competency entity.
    */
   public function setCreatedTime($timestamp);
@@ -51,7 +51,7 @@ interface KnowledgeCompetencyInterface extends ContentEntityInterface, RevisionL
    * @param int $timestamp
    *   The UNIX timestamp of when this revision was created.
    *
-   * @return \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @return \Drupal\knowledge\KnowledgeCompetencyInterface
    *   The called Competency entity.
    */
   public function setRevisionCreationTime($timestamp);
@@ -70,23 +70,16 @@ interface KnowledgeCompetencyInterface extends ContentEntityInterface, RevisionL
    * @param int $uid
    *   The user ID of the revision author.
    *
-   * @return \Drupal\knowledge\Entity\KnowledgeCompetencyInterface
+   * @return \Drupal\knowledge\KnowledgeCompetencyInterface
    *   The called Competency entity.
    */
   public function setRevisionUserId($uid);
 
-  /**
-   * Is the competency pending approval for knowledge_contributor.
-   */
-  public function isPendingContributor();
-
-  /**
-   * Is the competency pending approval for knowledge_publisher.
-   */
-  public function isPendingPublisher();
-
   /**
    * Is the competency pending approval.
+   *
+   * @return string|bool
+   *   The role id if the competency is pending approval.
    */
   public function isPending();
 
diff --git a/src/KnowledgeCompetencyServiceInterface.php b/src/KnowledgeCompetencyServiceInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..70151afca16840d17ed54239834b9431874b7b75
--- /dev/null
+++ b/src/KnowledgeCompetencyServiceInterface.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\knowledge;
+
+use Drupal\user\UserInterface;
+
+/**
+ * Interface for the Knowledge Competency service.
+ */
+interface KnowledgeCompetencyServiceInterface {
+
+  /**
+   * Get the roles.
+   *
+   * @return array
+   *   The roles.
+   */
+  public function get();
+
+  /**
+   * Get the role ids.
+   *
+   * @return array
+   *   The role ids.
+   */
+  public function getRoleIds();
+
+  /**
+   * Check if the user has the role or better.
+   *
+   * @param string $role
+   *   The role.
+   * @param array $user_roles
+   *   The user roles.
+   *
+   * @return bool
+   *   TRUE if the user has the role or better, FALSE otherwise.
+   */
+  public function hasRoleOrBetter($role, $user_roles);
+
+  /**
+   * Remove lesser competency roles.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user.
+   */
+  public function doRoleRemoval(UserInterface &$user);
+
+  /**
+   * Calculate values to be saved.
+   *
+   * @param array $roles
+   *   The roles.
+   * @param array $orginal_roles
+   *   The original roles.
+   */
+  public function doPreSave(&$roles, $orginal_roles);
+
+  /**
+   * Perform actions if any after saving.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user.
+   * @param array $roles
+   *   The roles.
+   * @param array $orginal_roles
+   *   The original roles.
+   */
+  public function doPostSave($user, $roles, $orginal_roles);
+
+  /**
+   * Get the user competency.
+   *
+   * @param int $user_id
+   *   The user.
+   *
+   * @return array
+   *   The user competency.
+   */
+  public function getUserCompetency($user_id);
+
+}
diff --git a/src/KnowledgeCompetencyStorage.php b/src/KnowledgeCompetencyStorage.php
index 4571a51a75a58a240babf3e39027f2a78d4116a0..3b284428b0e183cfca72a78ba0a3e48cfcdb9516 100644
--- a/src/KnowledgeCompetencyStorage.php
+++ b/src/KnowledgeCompetencyStorage.php
@@ -2,9 +2,16 @@
 
 namespace Drupal\knowledge;
 
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\knowledge\Entity\KnowledgeCompetencyInterface;
 
 /**
  * Defines the storage handler class for Competency entities.
@@ -16,6 +23,32 @@ use Drupal\knowledge\Entity\KnowledgeCompetencyInterface;
  */
 class KnowledgeCompetencyStorage extends SqlContentEntityStorage implements KnowledgeCompetencyStorageInterface {
 
+  /**
+   * Constructs a SqlContentEntityStorage object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend to be used.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
+   *   The memory cache backend to be used.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   The entity type bundle info.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($entity_type, $database, $entity_field_manager, $cache, $language_manager, $memory_cache, $entity_type_bundle_info, $entity_type_manager);
+    @trigger_error(__NAMESPACE__ . '\KnowledgeCompetencyStorage is deprecated in knowledge:8.x-1.4 and is removed from knowledge:2.0.0. Use \Drupal\knowledge\CompetencyStorage instead. See https://www.drupal.org/node/3476470', E_USER_DEPRECATED);
+
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/KnowledgeCompetencyStorageInterface.php b/src/KnowledgeCompetencyStorageInterface.php
index fcc3be2486a584bab8789835dc47a14b70ffdddf..a20547bb8e206f609a271cf8f6d924f4172af5d9 100644
--- a/src/KnowledgeCompetencyStorageInterface.php
+++ b/src/KnowledgeCompetencyStorageInterface.php
@@ -4,7 +4,6 @@ namespace Drupal\knowledge;
 
 use Drupal\Core\Entity\RevisionableStorageInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\knowledge\Entity\KnowledgeCompetencyInterface;
 
 /**
  * Defines the storage handler class for Competency entities.
@@ -19,7 +18,7 @@ interface KnowledgeCompetencyStorageInterface extends RevisionableStorageInterfa
   /**
    * Gets a list of Competency revision IDs for a specific Competency.
    *
-   * @param \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $entity
+   * @param \Drupal\knowledge\KnowledgeCompetencyInterface $entity
    *   The Competency entity.
    *
    * @return int[]
diff --git a/src/KnowledgeFieldItemList.php b/src/KnowledgeFieldItemList.php
index 104a4c47d50324611f3030d436148f3254d664d8..fd57d790588b07e8181f3771acbd87b30db1e7e6 100644
--- a/src/KnowledgeFieldItemList.php
+++ b/src/KnowledgeFieldItemList.php
@@ -43,7 +43,7 @@ class KnowledgeFieldItemList extends FieldItemList {
   /**
    * {@inheritdoc}
    */
-  public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
+  public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
     if ($operation === 'edit') {
       // Only users with administer knowledge permission can edit the knowledge
       // status field.
diff --git a/src/KnowledgeForm.php b/src/KnowledgeForm.php
index dd40909c89a0c3e25f5a3ddef37afd1e4afb5f28..01d905399b0d10c9c6d69375c4a1ee7ec37c58d3 100644
--- a/src/KnowledgeForm.php
+++ b/src/KnowledgeForm.php
@@ -75,7 +75,7 @@ class KnowledgeForm extends ContentEntityForm {
    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
    *   The entity field manager service.
    */
-  public function __construct(EntityRepositoryInterface $entity_repository, AccountInterface $current_user, RendererInterface $renderer, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) {
+  public function __construct(EntityRepositoryInterface $entity_repository, AccountInterface $current_user, RendererInterface $renderer, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, ?TimeInterface $time = NULL, ?EntityFieldManagerInterface $entity_field_manager = NULL) {
     parent::__construct($entity_repository, $entity_type_bundle_info, $time);
     $this->currentUser = $current_user;
     $this->renderer = $renderer;
diff --git a/src/KnowledgeStatistics.php b/src/KnowledgeStatistics.php
index b6059f411bf6f2231eaa1128aefd0b0825aebee1..82ae466f7ae9bf62d0219349df134ee3479f5783 100644
--- a/src/KnowledgeStatistics.php
+++ b/src/KnowledgeStatistics.php
@@ -145,7 +145,7 @@ class KnowledgeStatistics implements KnowledgeStatisticsInterface {
     ModuleHandlerInterface $module_handler,
     ConfigFactoryInterface $config_factory,
     MessengerInterface $messenger,
-    Connection $database_replica = NULL,
+    ?Connection $database_replica = NULL,
     ?ContentEntityTrackingManager $tracking_manager = NULL,
   ) {
     $this->database = $database;
diff --git a/src/KnowledgeTranslationHandler.php b/src/KnowledgeTranslationHandler.php
index a31984c9a9366c0ea9d2e3459a36a3b3461d09dc..1bfb6f14756dfdf4014c54b9257a45f35cee66cf 100644
--- a/src/KnowledgeTranslationHandler.php
+++ b/src/KnowledgeTranslationHandler.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\knowledge;
 
-use Drupal\content_translation\ContentTranslationHandler;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\content_translation\ContentTranslationHandler;
 
 /**
  * Defines the translation handler for knowledge.
diff --git a/src/Plugin/Action/ChangeUserWave.php b/src/Plugin/Action/ChangeUserWave.php
index 18e87038b4fd4d3d258ed1c24c8821959b0880ea..8e916e79e98c27b6c9435859b8f018133e0b41bb 100644
--- a/src/Plugin/Action/ChangeUserWave.php
+++ b/src/Plugin/Action/ChangeUserWave.php
@@ -76,7 +76,7 @@ class ChangeUserWave extends ConfigurableActionBase implements ContainerFactoryP
   /**
    * {@inheritdoc}
    */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+  public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
     /** @var \Drupal\user\UserInterface $object */
     $access = $object->access('update', $account, TRUE)
       ->andIf($object->roles->access('edit', $account, TRUE));
diff --git a/src/Plugin/Action/UnpublishByKeywordKnowledge.php b/src/Plugin/Action/UnpublishByKeywordKnowledge.php
index 13e3cda1ad909042992c0cc73f950fe3af460de6..756a766a981a3bdc6fad432dab74c0f6819be388 100644
--- a/src/Plugin/Action/UnpublishByKeywordKnowledge.php
+++ b/src/Plugin/Action/UnpublishByKeywordKnowledge.php
@@ -117,7 +117,7 @@ class UnpublishByKeywordKnowledge extends ConfigurableActionBase implements Cont
   /**
    * {@inheritdoc}
    */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+  public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
     /** @var \Drupal\knowledge\KnowledgeInterface $object */
     $result = $object->access('update', $account, TRUE)
       ->andIf($object->status->access('edit', $account, TRUE));
diff --git a/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyFormatter.php b/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyFormatter.php
index 428e3e13377cc1afca9f62e713b5a382898196dd..9ed50f0770078bc44676f2f5fb62d120e3ccce97 100644
--- a/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyFormatter.php
+++ b/src/Plugin/Field/FieldFormatter/KnowledgeCompetencyFormatter.php
@@ -6,7 +6,7 @@ use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\Plugin\Field\FieldFormatter\BooleanFormatter;
 
 /**
- * Plugin implementation of the 'KnowledgeCompetency' formatter.
+ * Plugin implementation of the 'Competency' formatter.
  *
  * @FieldFormatter(
  *   id = "knowledge_competency",
@@ -23,10 +23,10 @@ class KnowledgeCompetencyFormatter extends BooleanFormatter {
    */
   public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = parent::viewElements($items, $langcode);
+
     $definition = $items->getFieldDefinition();
     $description = $definition->getDescription();
     foreach ($items as $delta => $item) {
-      // Here you can customize how the boolean field is displayed.
       $elements[$delta]['#suffix'] = '<div>' . $description . '</div><br/>';
     }
 
diff --git a/src/Plugin/Menu/LocalTask/UnapprovedKnowledge.php b/src/Plugin/Menu/LocalTask/UnapprovedKnowledge.php
index bf057cc2c669002f99b51bc3a0d632415b77c6e8..885a27795dafdb5caedefaf9c3ba9970282d7f10 100644
--- a/src/Plugin/Menu/LocalTask/UnapprovedKnowledge.php
+++ b/src/Plugin/Menu/LocalTask/UnapprovedKnowledge.php
@@ -54,7 +54,7 @@ class UnapprovedKnowledge extends LocalTaskDefault implements ContainerFactoryPl
   /**
    * {@inheritdoc}
    */
-  public function getTitle(Request $request = NULL) {
+  public function getTitle(?Request $request = NULL) {
     return $this->t('Unlinked knowledge (@count)', ['@count' => $this->knowledgeStorage->getUnapprovedCount()]);
   }
 
diff --git a/src/Plugin/migrate/destination/EntityKnowledge.php b/src/Plugin/migrate/destination/EntityKnowledge.php
index 4ea8bc9659d26801b874a28312fcdd0f328aeb76..f1c5eaca557086514ec41fbc3d22a21e3b82a84b 100644
--- a/src/Plugin/migrate/destination/EntityKnowledge.php
+++ b/src/Plugin/migrate/destination/EntityKnowledge.php
@@ -7,8 +7,8 @@ use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Session\AccountSwitcherInterface;
 use Drupal\Core\State\StateInterface;
-use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
 use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
 use Drupal\migrate\Row;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -59,7 +59,7 @@ class EntityKnowledge extends EntityContentBase {
    * @param \Drupal\Core\Session\AccountSwitcherInterface|null $account_switcher
    *   The account switcher service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, StateInterface $state, AccountSwitcherInterface $account_switcher = NULL) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, StateInterface $state, ?AccountSwitcherInterface $account_switcher = NULL) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_field_manager, $field_type_manager, $account_switcher);
     $this->state = $state;
   }
@@ -67,7 +67,7 @@ class EntityKnowledge extends EntityContentBase {
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
     $entity_type = static::getEntityTypeId($plugin_id);
     return new static(
       $configuration,
diff --git a/src/Plugin/views/field/CompetencyApprove.php b/src/Plugin/views/field/CompetencyApprove.php
index e3b6751e28f0a32723e3797bb47444123c644ad3..6ca44092d9adbd309cac0660356b2316294f2679 100644
--- a/src/Plugin/views/field/CompetencyApprove.php
+++ b/src/Plugin/views/field/CompetencyApprove.php
@@ -6,7 +6,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Path\CurrentPathStack;
 use Drupal\Core\Url;
-use Drupal\user\Entity\Role;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\ResultRow;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -32,6 +32,13 @@ class CompetencyApprove extends FieldPluginBase {
    */
   protected $pathCurrent;
 
+  /**
+   * The competency service.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyServiceInterface
+   */
+  protected $competencyService;
+
   /**
    * {@inheritdoc}
    */
@@ -41,12 +48,13 @@ class CompetencyApprove extends FieldPluginBase {
       $plugin_id,
       $plugin_definition,
       $container->get('entity_type.manager'),
-      $container->get('path.current')
+      $container->get('path.current'),
+      $container->get('knowledge.competency'),
     );
   }
 
   /**
-   * Creates node type filter plugin.
+   * Creates knowledge competency filter plugin.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.
@@ -58,11 +66,22 @@ class CompetencyApprove extends FieldPluginBase {
    *   The entity type manager service.
    * @param \Drupal\Core\Path\CurrentPathStack $path_current
    *   The renderer service.
+   * @param \Drupal\knowledge\KnowledgeCompetencyServiceInterface $competency_service
+   *   The competency service.
    */
-  public function __construct(array $configuration, string $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, CurrentPathStack $path_current) {
+  public function __construct(
+    array $configuration,
+    string $plugin_id,
+    $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager,
+    CurrentPathStack $path_current,
+    KnowledgeCompetencyServiceInterface $competency_service,
+  ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
+
     $this->entityTypeManager = $entity_type_manager;
     $this->pathCurrent = $path_current;
+    $this->competencyService = $competency_service;
   }
 
   /**
@@ -82,24 +101,11 @@ class CompetencyApprove extends FieldPluginBase {
 
     $current_path = $this->pathCurrent->getPath();
     $link = NULL;
-    $competency_id = $this->entityTypeManager
-      ->getStorage('knowledge_competency')
-      ->getQuery()
-      ->condition('user_id', $entity_id)
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if (!count($competency_id)) {
-      return $link;
-    }
-    $competency_id = current($competency_id);
-    /** @var \Drupal\knowledge\Entity\KnowledgeCompetencyInterface $competency */
-    $competency = $this->entityTypeManager
-      ->getStorage('knowledge_competency')
-      ->load($competency_id);
-    if ($competency && $competency->isPending()) {
-      $role_id = $competency->isPendingPublisher() ? 'knowledge_publisher' : 'knowledge_contributor';
-      $role = Role::load($role_id);
+
+    $competency = $this->competencyService->getUserCompetency($entity_id);
+    $role_id = $competency->isPending();
+    if ($role_id) {
+      $role = $this->competencyService->getPromotionRole($role_id);
       $text = $this->t('Promote to @role', ['@role' => $role->label()]);
       $current_url = Url::fromUri("internal:$current_path")->toString();
       $url = Url::fromRoute(
diff --git a/src/Plugin/views/field/LastTimestamp.php b/src/Plugin/views/field/LastTimestamp.php
index 07b56f402d54a1f6ebe1caff2c3a2c20217f2fd7..61c6f3d479d0cabf2816c49200aff34535def6c9 100644
--- a/src/Plugin/views/field/LastTimestamp.php
+++ b/src/Plugin/views/field/LastTimestamp.php
@@ -19,7 +19,7 @@ class LastTimestamp extends Date {
   /**
    * {@inheritdoc}
    */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
     parent::init($view, $display, $options);
 
     $this->additional_fields['total_count'] = 'total_count';
diff --git a/src/Plugin/views/field/NodeNewKnowledge.php b/src/Plugin/views/field/NodeNewKnowledge.php
index 7306d2c33b3541060927e8529cc1233aa871608f..0a156fd154afcdc5a8dedf9f4fb718cd4c2beace 100644
--- a/src/Plugin/views/field/NodeNewKnowledge.php
+++ b/src/Plugin/views/field/NodeNewKnowledge.php
@@ -111,7 +111,7 @@ class NodeNewKnowledge extends NumericField {
   /**
    * {@inheritdoc}
    */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
     parent::init($view, $display, $options);
 
     $this->additional_fields['entity_id'] = 'nid';
diff --git a/src/Plugin/views/filter/KnowledgeDisposition.php b/src/Plugin/views/filter/KnowledgeDisposition.php
index 2cd08a4398bc8cfdbbd1c5d5b40f56d80ea4c5db..4ca3b25c41742f197b2fd2e78fda21ecfafb5de4 100644
--- a/src/Plugin/views/filter/KnowledgeDisposition.php
+++ b/src/Plugin/views/filter/KnowledgeDisposition.php
@@ -18,7 +18,7 @@ class KnowledgeDisposition extends InOperator {
   /**
    * {@inheritdoc}
    */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
     parent::init($view, $display, $options);
     $this->valueTitle = $this->t('Knowledge Disposition');
     $this->definition['options callback'] = [$this, 'generateOptions'];
diff --git a/src/Plugin/views/filter/KnowledgeLearners.php b/src/Plugin/views/filter/KnowledgeLearners.php
index c59e3a02eb221b0e616123329a95cd9a53e49ede..d880485540dffae5afa6591eac868e73ec490a97 100644
--- a/src/Plugin/views/filter/KnowledgeLearners.php
+++ b/src/Plugin/views/filter/KnowledgeLearners.php
@@ -68,7 +68,7 @@ class KnowledgeLearners extends InOperator {
   /**
    * {@inheritdoc}
    */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
     parent::init($view, $display, $options);
     $this->valueTitle = $this->t('Learners');
     $this->definition['options callback'] = [$this, 'generateOptions'];
diff --git a/src/Plugin/views/filter/KnowledgeNodeType.php b/src/Plugin/views/filter/KnowledgeNodeType.php
index b55a3b4cd294977ca7b06e7ca84257a8d9d03c78..055d83cccabc5189eacff28fa600be038e010f24 100644
--- a/src/Plugin/views/filter/KnowledgeNodeType.php
+++ b/src/Plugin/views/filter/KnowledgeNodeType.php
@@ -68,7 +68,7 @@ class KnowledgeNodeType extends InOperator {
   /**
    * {@inheritdoc}
    */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
     parent::init($view, $display, $options);
     $this->valueTitle = $this->t('Allowed node types');
     $this->definition['options callback'] = [$this, 'generateOptions'];
diff --git a/src/Service/CompetencyService.php b/src/Service/CompetencyService.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae1d13cb6a92ce03f072b8939b435ab83ed80f07
--- /dev/null
+++ b/src/Service/CompetencyService.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace Drupal\knowledge\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Messenger\MessengerTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\knowledge\KnowledgeCompetencyServiceInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Service for the Competency entity.
+ */
+class CompetencyService implements KnowledgeCompetencyServiceInterface {
+  use StringTranslationTrait;
+  use MessengerTrait;
+
+  /**
+   * The settings.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $settings;
+
+  /**
+   * The competency storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $competencyStorage;
+
+  /**
+   * The user role storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $userRoleStorage;
+
+  /**
+   * Constructs a new CompetencyService object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param mixed $string_translation
+   *   The string translation service.
+   * @param mixed $messenger
+   *   The messenger service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, $string_translation, $messenger) {
+    $this->settings = $config_factory->get('knowledge.competency.settings');
+    $this->competencyStorage = $entity_type_manager->getStorage('knowledge_competency');
+    $this->userRoleStorage = $entity_type_manager->getStorage('user_role');
+    $this->setStringTranslation($string_translation);
+    $this->setMessenger($messenger);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get() {
+    $setting = $this->settings->get('roles') ?? [];
+    usort($setting, [static::class, 'sort']);
+
+    return $setting;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUserCompetency($user_id) {
+    $query = $this->competencyStorage->getQuery();
+    $result = $query
+      ->condition('user_id', $user_id)
+      ->accessCheck(FALSE)
+      ->execute();
+    $competency = NULL;
+    if (count($result) == 1) {
+      $competency = $this->competencyStorage->load(current($result));
+    }
+    if (count($result) == 0) {
+      $competency = $this->competencyStorage->create([
+        'user_id' => $user_id,
+      ]);
+    }
+
+    return $competency;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoleIds() {
+    $roles = $this->get();
+    $role_ids = [];
+    foreach ($roles as $role) {
+      $role_ids[] = $role['role'];
+    }
+    return $role_ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRoleOrBetter($role, $user_roles) {
+    if (in_array($role, $user_roles)) {
+      return TRUE;
+    }
+
+    $roles = $this->getRoleIds();
+    $role_index = array_search($role, $roles);
+    if ($role_index === FALSE) {
+      return FALSE;
+    }
+
+    foreach (range($role_index + 1, count($roles) - 1) as $i) {
+      if (in_array($roles[$i] ?? NULL, $user_roles)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doRoleRemoval(UserInterface &$user) {
+    $user_roles = $user->getRoles(TRUE);
+    $removable_roles = $this->getRemovableRoles($user_roles);
+    foreach ($removable_roles as $role) {
+      $user->removeRole($role);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doPreSave(&$roles, $orginal_roles) {
+
+    $role_settings = $this->get();
+    $sorted_roles = $this->getRoleIds();
+    $existing_completed = [];
+
+    foreach ($orginal_roles as $role) {
+      $completed = $role['total'] == $role['correct'] && $role['approved'] != NULL;
+      if ($completed) {
+        $existing_completed[] = $role['role'];
+      }
+    }
+    $new_roles = [];
+    foreach ($roles as $role) {
+      $role_name = $role->role;
+      $complete = $role->total == $role->correct;
+      if (!$complete) {
+        continue;
+      }
+      $exising = in_array($role_name, $existing_completed);
+      if ($exising || $role->proposed != NULL) {
+        continue;
+      }
+      $new_roles[] = $role_name;
+    }
+
+    $completed_roles = [];
+    $settings = [];
+    foreach ($role_settings as $setting) {
+      $role = $setting['role'];
+      $settings[$role] = $setting;
+      $is_existing = in_array($role, $existing_completed);
+      $is_new = in_array($role, $new_roles);
+      $completed = !$is_existing && $is_new;
+      if ($completed) {
+        $completed_roles[] = $role;
+      }
+      elseif ($is_existing) {
+        continue;
+      }
+      else {
+        break;
+      }
+    }
+
+    foreach ($roles as $role) {
+      $role_name = $role->role;
+      $completed = in_array($role_name, $completed_roles);
+      if (!$completed) {
+        continue;
+      }
+      $action = $settings[$role_name]['action'] ?? '_none';
+
+      $now = time();
+      $curren_user_id = \Drupal::currentUser()->id();
+
+      switch ($action) {
+
+        case 'auto':
+          $role->proposed = $now;
+          $role->proposer = $curren_user_id;
+          $role->approved = $now;
+          $role->approver = $curren_user_id;
+          break;
+
+        case 'leader':
+          $role->proposed = $now;
+          $role->proposer = $curren_user_id;
+          break;
+
+        case '_none':
+        default:
+          $role->proposed = $now;
+          $role->approved = $now;
+          break;
+      }
+    }
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doPostSave($user, $roles, $orginal_roles) {
+    $approved_roles = [];
+    foreach ($roles as $role) {
+      if ($role['approved'] != NULL) {
+        $approved_roles[] = $role['role'];
+      }
+    }
+    $orginal_approved_roles = [];
+    foreach ($orginal_roles as $role) {
+      if ($role['approved'] != NULL) {
+        $orginal_approved_roles[] = $role['role'];
+      }
+    }
+
+    $new_approved_roles = array_diff($approved_roles, $orginal_approved_roles);
+    $settings = $this->get();
+    $role_ids = $this->getRoleIds();
+    $save_user = FALSE;
+    $user_roles = $user->getRoles(TRUE);
+    foreach ($settings as $setting) {
+      $role = $setting['role'];
+      if (!in_array($role, $new_approved_roles)) {
+        continue;
+      }
+
+      $promote = $setting['promote'] ?? '_none';
+      $promote_role_id = NULL;
+      switch ($promote) {
+        case 'self':
+          $needs_role = !$this->hasRoleOrBetter($role, $user_roles);
+          if ($needs_role) {
+            $user->addRole($role);
+            $promote_role_id = $role;
+            $save_user = TRUE;
+          }
+          break;
+
+        case 'next':
+          $next_role = $role_ids[array_search($role, $role_ids) + 1] ?? NULL;
+          if (!is_null($next_role)) {
+            $needs_role = !$this->hasRoleOrBetter($next_role, $user_roles);
+            if ($needs_role) {
+              $user->addRole($next_role);
+              $promote_role_id = $next_role;
+              $save_user = TRUE;
+            }
+          }
+          break;
+
+        case '_none':
+        default:
+          break;
+      }
+
+    }
+
+    if ($save_user) {
+      $user->save();
+      $promote_role = $this->userRoleStorage->load($promote_role_id);
+      $this->messenger()->addMessage($this->t('%user was promoted to %role.', [
+        '%role' => $promote_role->label(),
+        '%user' => $user->getDisplayName(),
+      ]));
+    }
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPromotionRole($role_id) {
+    $roles = $this->get();
+    $role_ids = $this->getRoleIds();
+    $role_index = array_search($role_id, $role_ids);
+    if ($role_index === FALSE) {
+      return NULL;
+    }
+
+    $next_role = $role_ids[$role_index + 1] ?? NULL;
+
+    $settings = $this->get();
+    $role_settings = [];
+    foreach ($settings as $setting) {
+      $role_settings[$setting['role']] = $setting;
+    }
+
+    $promote = $role_settings[$role_id]['promote'] ?? '_none';
+    switch ($promote) {
+      case 'self':
+        return $role_id;
+
+      case 'next':
+        return $next_role;
+
+      case '_none':
+      default:
+        return NULL;
+    }
+
+  }
+
+  /**
+   * Get the roles that can be removed.
+   *
+   * @param array $user_roles
+   *   The user roles.
+   *
+   * @return array
+   *   The roles that can be removed.
+   */
+  private function getRemovableRoles($user_roles) {
+
+    $removable_roles = [];
+    $best_role = NULL;
+    $roles = $this->getRoleIds();
+    foreach ($roles as $index => $role) {
+      if (in_array($role, $user_roles)) {
+        if (!is_null($best_role)) {
+          $removable_roles[] = $best_role;
+        }
+
+        $best_role = $role;
+      }
+    }
+
+    return $removable_roles;
+  }
+
+  /**
+   * Callback for usort.
+   */
+  private static function sort($a, $b) {
+    if ($a['weight'] == $b['weight']) {
+      return 0;
+    }
+    return ($a['weight'] < $b['weight']) ? -1 : 1;
+  }
+
+}
diff --git a/src/Service/KnowledgeLeaderService.php b/src/Service/KnowledgeLeaderService.php
index 2881c598cb106f3c8fc5cd7c29871a80ba5f91da..88a4ade4c6d04ebb2a7fc47353199405d1087641 100644
--- a/src/Service/KnowledgeLeaderService.php
+++ b/src/Service/KnowledgeLeaderService.php
@@ -95,7 +95,7 @@ class KnowledgeLeaderService implements KnowledgeLeaderInterface {
       $leader->removeRole('knowledge_leader');
       $leader->save();
     }
-    elseif (!in_array('knowledge_leader', $leader->getRoles())) {
+    elseif (!in_array('knowledge_leader', $leader->getRoles(TRUE))) {
       $leader->addRole('knowledge_leader');
       $leader->save();
     }
diff --git a/tests/src/Functional/CompetencyPromotionTest.php b/tests/src/Functional/CompetencyPromotionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..771128801f1139a773996787b6bed779e09c376f
--- /dev/null
+++ b/tests/src/Functional/CompetencyPromotionTest.php
@@ -0,0 +1,476 @@
+<?php
+
+namespace Drupal\Tests\knowledge\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests knowledge administration and preview access.
+ *
+ * @group knowledge
+ */
+class CompetencyPromotionTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'knowledge',
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * The learner user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $learner;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->learner = $this->drupalCreateUser([
+      'access knowledge',
+      'access content',
+      'view own knowledge_competency',
+    ]);
+
+  }
+
+  /**
+   * Tests role advancement, auto-promotion, current role.
+   */
+  public function testCompetencyAutoSelf() {
+    $learner_id = $this->learner->id();
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $competency_url = "user/$learner_id/competency";
+    $edit_url = "user/$learner_id/edit";
+    // The coach is not the learner's coach yet.
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(403);
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doCanidate();
+
+    $this->assertSession()->pageTextContains('was promoted to Candidate');
+
+    $this->doContributor();
+
+    $this->assertSession()->pageTextContains('was promoted to Contributor');
+
+    $this->doPublisher();
+
+    $this->assertSession()->pageTextContains('was promoted to Publisher');
+
+  }
+
+  /**
+   * Tests role advancement, auto-promotion, next role.
+   */
+  public function testCompetencyAutoNext() {
+    $config = \Drupal::service('config.factory')->getEditable('knowledge.competency.settings');
+    $settings = $config->get('roles');
+    foreach ($settings as $index => $value) {
+      $settings[$index]['promote'] = 'next';
+      if ($value['role'] == 'publisher') {
+        $settings[$index]['promote'] = '_none';
+        $settings[$index]['action'] = '_none';
+      }
+    }
+    $config->set('roles', $settings)->save();
+
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $learner_id = $this->learner->id();
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $competency_url = "user/$learner_id/competency";
+    $edit_url = "user/$learner_id/edit";
+    // The coach is not the learner's coach yet.
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(403);
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doCanidate();
+
+    $this->assertSession()->pageTextContains('was promoted to Contributor');
+
+    $this->doContributor();
+
+    $this->assertSession()->pageTextContains('was promoted to Publisher');
+
+    $this->doPublisher();
+
+  }
+
+  /**
+   * Tests role advancement, leader appoval, corrunt role.
+   */
+  public function testCompetencyLeaderSelf() {
+    $config = \Drupal::service('config.factory')->getEditable('knowledge.competency.settings');
+    $settings = $config->get('roles');
+    foreach ($settings as $index => $value) {
+      $settings[$index]['action'] = 'leader';
+    }
+    $config->set('roles', $settings)->save();
+
+    $leader = $this->drupalCreateUser([
+      'view follower knowledge_competency',
+    ]);
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $learner_id = $this->learner->id();
+    $competency_url = "user/$learner_id/competency";
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->knowledge_leader->target_id = $leader->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doCanidate();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(200);
+
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->submitForm([], 'Promote');
+
+    $this->assertSession()->pageTextContains('was promoted to Candidate');
+
+    $this->drupalLogin($coach);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->doContributor();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(200);
+
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->submitForm([], 'Promote');
+
+    $this->assertSession()->pageTextContains('was promoted to Contributor');
+
+    $this->drupalLogin($coach);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->doPublisher();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(200);
+
+    $this->assertSession()->pageTextContains('Publisher');
+    $this->submitForm([], 'Promote');
+
+    $this->assertSession()->pageTextContains('was promoted to Publisher');
+
+  }
+
+  /**
+   * Tests role advancement, leader appoval, corrunt role.
+   */
+  public function testCompetencyLeaderNext() {
+    $config = \Drupal::service('config.factory')->getEditable('knowledge.competency.settings');
+    $settings = $config->get('roles');
+    foreach ($settings as $index => $value) {
+      $settings[$index]['action'] = 'leader';
+      $settings[$index]['promote'] = 'next';
+      if ($value['role'] == 'knowledge_publisher') {
+        $settings[$index]['promote'] = '_none';
+        $settings[$index]['action'] = '_none';
+      }
+    }
+    $config->set('roles', $settings)->save();
+
+    $leader = $this->drupalCreateUser([
+      'view follower knowledge_competency',
+    ]);
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $learner_id = $this->learner->id();
+    $competency_url = "user/$learner_id/competency";
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->knowledge_leader->target_id = $leader->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doCanidate();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(200);
+
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->submitForm([], 'Promote');
+
+    $this->assertSession()->pageTextContains('was promoted to Contributor');
+
+    $this->drupalLogin($coach);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->doContributor();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(200);
+
+    $this->assertSession()->pageTextContains('Publisher');
+    $this->submitForm([], 'Promote');
+
+    $this->assertSession()->pageTextContains('was promoted to Publisher');
+
+    $this->drupalLogin($coach);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->doPublisher();
+
+    $this->drupalLogin($leader);
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+
+    $this->drupalGet($competency_url . '/approve');
+    $assert->statusCodeEquals(404);
+
+    // $this->assertSession()->pageTextContains('Publisher');
+    // $this->submitForm([], 'Promote');
+    // $this->assertSession()->pageTextContains('was promoted to Publisher');
+  }
+
+  /**
+   * Tests role advancement, _none, on role.
+   */
+  public function testCompetencyNone() {
+    $config = \Drupal::service('config.factory')->getEditable('knowledge.competency.settings');
+    $settings = $config->get('roles');
+    foreach ($settings as $index => $value) {
+      $settings[$index]['action'] = '_none';
+      $settings[$index]['promote'] = '_none';
+    }
+    $config->set('roles', $settings)->save();
+
+    $learner_id = $this->learner->id();
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $competency_url = "user/$learner_id/competency";
+    $edit_url = "user/$learner_id/edit";
+    // The coach is not the learner's coach yet.
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(403);
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doCanidate();
+
+    $this->assertSession()->pageTextNotContains('was promoted to Candidate');
+
+    $this->doContributor();
+
+    $this->assertSession()->pageTextNotContains('was promoted to Contributor');
+
+    $this->doPublisher();
+
+    $this->assertSession()->pageTextNotContains('was promoted to Publisher');
+
+  }
+
+  /**
+   * Tests role advancement, auto-promotion, current role.
+   */
+  public function testCompetencyAutoSelfAll() {
+    $learner_id = $this->learner->id();
+    $coach = $this->drupalCreateUser([
+      'edit learner knowledge_competency',
+    ]);
+    $this->drupalLogin($coach);
+
+    $assert = $this->assertSession();
+
+    $competency_url = "user/$learner_id/competency";
+    $edit_url = "user/$learner_id/edit";
+    // The coach is not the learner's coach yet.
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(403);
+
+    // The coach is the learner's coach.
+    $this->learner->knowledge_coach->target_id = $coach->id();
+    $this->learner->save();
+
+    $this->drupalGet($competency_url);
+    $assert->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('Candidate');
+    $this->assertSession()->pageTextContains('Contributor');
+    $this->assertSession()->pageTextContains('Publisher');
+
+    $this->doPublisher();
+
+    $this->assertSession()->pageTextContains('was promoted to Publisher');
+
+  }
+
+  /**
+   * Submit the form with Candidate values.
+   */
+  protected function doCanidate() {
+    $this->submitForm([
+      'edit-field-search-it-value' => 1,
+      'edit-field-link-it-value' => 1,
+      'edit-field-flag-it-value' => 1,
+    ], 'Save');
+  }
+
+  /**
+   * Submit the form with Contributor values.
+   */
+  protected function doContributor() {
+    $this->submitForm([
+      'edit-field-search-it-value' => 1,
+      'edit-field-link-it-value' => 1,
+      'edit-field-flag-it-value' => 1,
+
+      'edit-field-business-value' => 1,
+      'edit-field-documents-request-value' => 1,
+      'edit-field-fix-it-value' => 1,
+      'edit-field-reuse-value' => 1,
+      'edit-field-kcs-article-elements-value' => 1,
+      'edit-field-structure-value' => 1,
+      'edit-field-complete-thoughts-value' => 1,
+      'edit-field-one-value' => 1,
+      'edit-field-includes-context-value' => 1,
+      'edit-field-update-or-create-value' => 1,
+      'edit-field-solve-loop-value' => 1,
+
+    ], 'Save');
+  }
+
+  /**
+   * Submit the form with publisher values.
+   */
+  protected function doPublisher() {
+    $this->submitForm([
+      'edit-field-search-it-value' => 1,
+      'edit-field-link-it-value' => 1,
+      'edit-field-flag-it-value' => 1,
+
+      'edit-field-business-value' => 1,
+      'edit-field-documents-request-value' => 1,
+      'edit-field-fix-it-value' => 1,
+      'edit-field-reuse-value' => 1,
+      'edit-field-kcs-article-elements-value' => 1,
+      'edit-field-structure-value' => 1,
+      'edit-field-complete-thoughts-value' => 1,
+      'edit-field-one-value' => 1,
+      'edit-field-includes-context-value' => 1,
+      'edit-field-update-or-create-value' => 1,
+      'edit-field-solve-loop-value' => 1,
+
+      'edit-field-capture-context-value' => 1,
+      'edit-field-audience-value' => 1,
+      'edit-field-content-standard-value' => 1,
+      'edit-field-sufficient-to-solve-value' => 1,
+      'edit-field-relevant-value' => 1,
+      'edit-field-iterative-search-value' => 1,
+      'edit-field-improve-value' => 1,
+      'edit-field-process-adherence-value' => 1,
+      'edit-field-confidence-value' => 1,
+      'edit-field-capture-in-the-moment-value' => 1,
+      'edit-field-collaborate-value' => 1,
+
+    ], 'Save');
+  }
+
+}
diff --git a/tests/src/Functional/KnowledgeAccessTest.php b/tests/src/Functional/KnowledgeAccessTest.php
index 8deea28e99b50fa0cff9f650492857b72b10242e..49a6d624969e7ddc63d148537324f7ab8dd909df 100644
--- a/tests/src/Functional/KnowledgeAccessTest.php
+++ b/tests/src/Functional/KnowledgeAccessTest.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\Tests\knowledge\Functional;
 
+use Drupal\Tests\BrowserTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests knowledge administration and preview access.
diff --git a/tests/src/Functional/KnowledgeBookTest.php b/tests/src/Functional/KnowledgeBookTest.php
deleted file mode 100644
index 6fa9b874d354065e3cf432310ea462f36bfb728c..0000000000000000000000000000000000000000
--- a/tests/src/Functional/KnowledgeBookTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace Drupal\Tests\knowledge\Functional;
-
-use Drupal\knowledge\Entity\Knowledge;
-use Drupal\knowledge\KnowledgeInterface;
-use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\node\Entity\Node;
-use Drupal\Tests\BrowserTestBase;
-
-/**
- * Tests visibility of knowledge on book pages.
- *
- * @group knowledge
- */
-class KnowledgeBookTest extends BrowserTestBase {
-
-  use KnowledgeTestTrait;
-
-  /**
-   * Modules to install.
-   *
-   * @var array
-   */
-  protected static $modules = ['book', 'knowledge'];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $defaultTheme = 'stark';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp(): void {
-    parent::setUp();
-
-    // Create knowledge field on book.
-    $this->addDefaultKnowledgeField('node', 'book');
-  }
-
-  /**
-   * Tests knowledge in book export.
-   */
-  public function testBookKnowledgePrint() {
-    $book_node = Node::create([
-      'type' => 'book',
-      'title' => 'Book title',
-      'body' => 'Book body',
-    ]);
-    $book_node->book['bid'] = 'new';
-    $book_node->save();
-
-    $knowledge = Knowledge::create([
-      'entity_id' => $book_node->id(),
-      'entity_type' => 'node',
-      'field_name' => 'knowledge',
-      'status' => KnowledgeInterface::PUBLISHED,
-    ]);
-    $knowledge->save();
-
-    $knowledge_user = $this->drupalCreateUser([
-      'access printer-friendly version',
-      'access knowledge',
-      'post knowledge',
-    ]);
-    $this->drupalLogin($knowledge_user);
-
-    $this->drupalGet('node/' . $book_node->id());
-
-    $this->assertSession()->pageTextContains('Add new knowledge');
-    // Ensure that the knowledge form subject field exists.
-    $this->assertSession()->fieldExists('subject[0][value]');
-
-    $this->drupalGet('book/export/html/' . $book_node->id());
-
-    $this->assertSession()->pageTextContains('Knowledge');
-
-    $this->assertSession()->pageTextNotContains('Add new knowledge');
-    // Verify that the knowledge form subject field is not found.
-    $this->assertSession()->fieldNotExists('subject[0][value]');
-  }
-
-}
diff --git a/tests/src/Functional/KnowledgeCSSTest.php b/tests/src/Functional/KnowledgeCSSTest.php
index 3f3c0083e9b343cc16814f26fd130edcc0e1c0e4..799ca8e96ae0fa60c489f17043aa1f99420c1c5d 100644
--- a/tests/src/Functional/KnowledgeCSSTest.php
+++ b/tests/src/Functional/KnowledgeCSSTest.php
@@ -3,9 +3,9 @@
 namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\KnowledgeInterface;
-use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
 use Drupal\user\RoleInterface;
 
 /**
diff --git a/tests/src/Functional/KnowledgeCacheTagsTest.php b/tests/src/Functional/KnowledgeCacheTagsTest.php
index c1ccce745b5492af5e8d6931f33342ccff09fbf0..95e869219ad5f9fb77188399ff3e9dce2394f7b2 100644
--- a/tests/src/Functional/KnowledgeCacheTagsTest.php
+++ b/tests/src/Functional/KnowledgeCacheTagsTest.php
@@ -4,13 +4,13 @@ namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\KnowledgeManagerInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
 
diff --git a/tests/src/Functional/KnowledgeEntityTest.php b/tests/src/Functional/KnowledgeEntityTest.php
index 6dd0ea9b7a7c512a77adc82cbb3cef55b1f808f1..55b0506d4573f035331d4b55f8f6f8ae5722b22a 100644
--- a/tests/src/Functional/KnowledgeEntityTest.php
+++ b/tests/src/Functional/KnowledgeEntityTest.php
@@ -3,11 +3,11 @@
 namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
-use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
 
 /**
  * Tests knowledge with other entities.
diff --git a/tests/src/Functional/KnowledgeLanguageTest.php b/tests/src/Functional/KnowledgeLanguageTest.php
index c80811a54e0a0c85f1bcb67661ff82bd3441542a..c8c9b221caa355fa4ad861a7a76a93afe986fb82 100644
--- a/tests/src/Functional/KnowledgeLanguageTest.php
+++ b/tests/src/Functional/KnowledgeLanguageTest.php
@@ -3,11 +3,11 @@
 namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Tests\BrowserTestBase;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests for knowledge language.
diff --git a/tests/src/Functional/KnowledgeNonNodeTest.php b/tests/src/Functional/KnowledgeNonNodeTest.php
index cab1e5bb9859dfb1ef26e950f91763167e31c92f..db577b6d375472dd82ca8c754b7ac417e6848893 100644
--- a/tests/src/Functional/KnowledgeNonNodeTest.php
+++ b/tests/src/Functional/KnowledgeNonNodeTest.php
@@ -3,6 +3,8 @@
 namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
@@ -11,8 +13,6 @@ use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\BrowserTestBase;
-use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 use Drupal\user\RoleInterface;
 
 /**
@@ -200,7 +200,7 @@ class KnowledgeNonNodeTest extends BrowserTestBase {
    * @return bool
    *   Boolean indicating whether the knowledge was found.
    */
-  public function knowledgeExists(KnowledgeInterface $knowledge = NULL, $reply = FALSE) {
+  public function knowledgeExists(?KnowledgeInterface $knowledge = NULL, $reply = FALSE) {
     if ($knowledge) {
       $regex = '/' . ($reply ? '<div class="indented">(.*?)' : '');
       $regex .= '<article(.*?)id="knowledge-' . $knowledge->id() . '"(.*?)';
diff --git a/tests/src/Functional/KnowledgePreviewTest.php b/tests/src/Functional/KnowledgePreviewTest.php
index 9a364e1c79179e29cd780833e06630c6faa9ad33..5a72fe674fb73efd07271612e11ff1d9947696de 100644
--- a/tests/src/Functional/KnowledgePreviewTest.php
+++ b/tests/src/Functional/KnowledgePreviewTest.php
@@ -4,9 +4,9 @@ namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Component\Render\MarkupInterface;
 use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Tests\TestFileCreationTrait;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\KnowledgeManagerInterface;
-use Drupal\Tests\TestFileCreationTrait;
 
 /**
  * Tests knowledge preview.
diff --git a/tests/src/Functional/KnowledgeRssTest.php b/tests/src/Functional/KnowledgeRssTest.php
index 6f1ca84c6058c9eec4c91769bd40f6f0ee80842a..5b0b2317734050d4da4b76808f370d583de4a75b 100644
--- a/tests/src/Functional/KnowledgeRssTest.php
+++ b/tests/src/Functional/KnowledgeRssTest.php
@@ -4,8 +4,8 @@ namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
-use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
+use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 
 /**
  * Tests knowledge as part of an RSS feed.
diff --git a/tests/src/Functional/KnowledgeStatusFieldAccessTest.php b/tests/src/Functional/KnowledgeStatusFieldAccessTest.php
index 82a6130ddc6df5f6580ff12d142dd18a160da969..43e56074c9df3642451ee959508c0dfafd440173 100644
--- a/tests/src/Functional/KnowledgeStatusFieldAccessTest.php
+++ b/tests/src/Functional/KnowledgeStatusFieldAccessTest.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\Tests\knowledge\Functional;
 
+use Drupal\Tests\BrowserTestBase;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests knowledge status field access.
diff --git a/tests/src/Functional/KnowledgeTestBase.php b/tests/src/Functional/KnowledgeTestBase.php
index 8ead869b567431a119b30a58af370ebd73015e90..5bbd599f670449e495532d438ba1c2fcb327e38a 100644
--- a/tests/src/Functional/KnowledgeTestBase.php
+++ b/tests/src/Functional/KnowledgeTestBase.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\knowledge\Functional;
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Tests\BrowserTestBase;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
@@ -11,7 +12,6 @@ use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\BrowserTestBase;
 
 /**
  * Provides setup and helper methods for knowledge tests.
@@ -209,7 +209,7 @@ abstract class KnowledgeTestBase extends BrowserTestBase {
    * @return bool
    *   Boolean indicating whether the knowledge was found.
    */
-  public function knowledgeExists(KnowledgeInterface $knowledge = NULL, $reply = FALSE) {
+  public function knowledgeExists(?KnowledgeInterface $knowledge = NULL, $reply = FALSE) {
     if ($knowledge) {
       $knowledge_element = $this->cssSelect(($reply ? '.indented ' : '') . 'article#knowledge-' . $knowledge->id());
       if (empty($knowledge_element)) {
diff --git a/tests/src/Functional/KnowledgeTranslationUITest.php b/tests/src/Functional/KnowledgeTranslationUITest.php
index dad2cebb933f7d62fd5ae3554e5b374bb9fb6d27..faa0e11167bd96784872d969febca2396ecc16f4 100644
--- a/tests/src/Functional/KnowledgeTranslationUITest.php
+++ b/tests/src/Functional/KnowledgeTranslationUITest.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\Tests\knowledge\Functional;
 
+use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\language\Entity\ConfigurableLanguage;
-use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
 
 /**
  * Tests the Knowledge Translation UI.
diff --git a/tests/src/Functional/Rest/KnowledgeResourceTestBase.php b/tests/src/Functional/Rest/KnowledgeResourceTestBase.php
index 5c99e74e8c2d5c5d2ded7dd4dd9f3707b6778762..0964928bc52d92a817679f87bbf868bc30b3ac5d 100644
--- a/tests/src/Functional/Rest/KnowledgeResourceTestBase.php
+++ b/tests/src/Functional/Rest/KnowledgeResourceTestBase.php
@@ -3,11 +3,11 @@
 namespace Drupal\Tests\knowledge\Functional\Rest;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
 
diff --git a/tests/src/Functional/Rest/KnowledgeTypeResourceTestBase.php b/tests/src/Functional/Rest/KnowledgeTypeResourceTestBase.php
index 17719863f17a2cfee0f8b62cdf731bb0195a1a33..14efc0cad7909470a4f4da80ea611401caf5a6f7 100644
--- a/tests/src/Functional/Rest/KnowledgeTypeResourceTestBase.php
+++ b/tests/src/Functional/Rest/KnowledgeTypeResourceTestBase.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\knowledge\Functional\Rest;
 
-use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
+use Drupal\knowledge\Entity\KnowledgeType;
 
 /**
  * ResourceTestBase for KnowledgeType entity.
diff --git a/tests/src/Functional/Views/DefaultViewRecentKnowledgesTest.php b/tests/src/Functional/Views/DefaultViewRecentKnowledgesTest.php
index b9a9b64fb3d3f9345e4681afba054ff18aab56d1..eb029d2e4ec6272cfded755f1e969727ef84e639 100644
--- a/tests/src/Functional/Views/DefaultViewRecentKnowledgesTest.php
+++ b/tests/src/Functional/Views/DefaultViewRecentKnowledgesTest.php
@@ -3,10 +3,10 @@
 namespace Drupal\Tests\knowledge\Functional\Views;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Tests\views\Functional\ViewTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\views\Functional\ViewTestBase;
 use Drupal\views\Views;
 
 /**
diff --git a/tests/src/Functional/Views/KnowledgeAdminTest.php b/tests/src/Functional/Views/KnowledgeAdminTest.php
index 30dc1bf20a8b28677c73d5de9e8314f830083bcc..af270c775bcca950669967d3107e64c36345391f 100644
--- a/tests/src/Functional/Views/KnowledgeAdminTest.php
+++ b/tests/src/Functional/Views/KnowledgeAdminTest.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\knowledge\Functional\Views;
 
-use Drupal\block_content\Entity\BlockContent;
-use Drupal\block_content\Entity\BlockContentType;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Tests\knowledge\Functional\KnowledgeTestBase as KnowledgeBrowserTestBase;
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
-use Drupal\Tests\knowledge\Functional\KnowledgeTestBase as KnowledgeBrowserTestBase;
 use Drupal\user\RoleInterface;
 use Drupal\views\Views;
 
diff --git a/tests/src/Functional/Views/KnowledgeTestBase.php b/tests/src/Functional/Views/KnowledgeTestBase.php
index 49bc71a6322ccd2ae166e89d86c018d87dcb86cc..81f14bcefc60b139cb0d0cea8b4894944fc6b7eb 100644
--- a/tests/src/Functional/Views/KnowledgeTestBase.php
+++ b/tests/src/Functional/Views/KnowledgeTestBase.php
@@ -3,9 +3,9 @@
 namespace Drupal\Tests\knowledge\Functional\Views;
 
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Tests\views\Functional\ViewTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\views\Functional\ViewTestBase;
 
 /**
  * Provides setup and helper methods for knowledge views tests.
diff --git a/tests/src/Functional/Views/WizardTest.php b/tests/src/Functional/Views/WizardTest.php
index aaef4604b38d9b1cf1d8efe23fadc0180913086a..109cfb195d0a924c81f0a62b4e2e7ba3a0705737 100644
--- a/tests/src/Functional/Views/WizardTest.php
+++ b/tests/src/Functional/Views/WizardTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\knowledge\Functional\Views;
 
-use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\Tests\views\Functional\Wizard\WizardTestBase;
+use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\views\Views;
 
 /**
diff --git a/tests/src/Kernel/KnowledgeActionsTest.php b/tests/src/Kernel/KnowledgeActionsTest.php
index 53ceafef44fea9f042fb39eae2453a63fff3714c..732dc7f7be061b13f13f1435e4da6672a3b1e835 100644
--- a/tests/src/Kernel/KnowledgeActionsTest.php
+++ b/tests/src/Kernel/KnowledgeActionsTest.php
@@ -3,9 +3,9 @@
 namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\filter\Entity\FilterFormat;
-use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
diff --git a/tests/src/Kernel/KnowledgeBaseFieldTest.php b/tests/src/Kernel/KnowledgeBaseFieldTest.php
index 4d7861aed186b6fd030cfa895b556df4e408f08d..165ea728104bdf19af1a46253986fb9c7faf8ce8 100644
--- a/tests/src/Kernel/KnowledgeBaseFieldTest.php
+++ b/tests/src/Kernel/KnowledgeBaseFieldTest.php
@@ -41,7 +41,6 @@ class KnowledgeBaseFieldTest extends KernelTestBase {
     parent::setUp();
     $this->installEntitySchema('knowledge_test_base_field');
     $this->installEntitySchema('knowledge');
-    $this->installSchema('system', ['sequences']);
     $this->installEntitySchema('user');
   }
 
diff --git a/tests/src/Kernel/KnowledgeDefaultFormatterCacheTagsTest.php b/tests/src/Kernel/KnowledgeDefaultFormatterCacheTagsTest.php
index 76a7329e99a204b0dbbf2dfe4ccac8b1f2c94f93..2cf205f2bed6f347d1111157ff6f3709b9e9b39b 100644
--- a/tests/src/Kernel/KnowledgeDefaultFormatterCacheTagsTest.php
+++ b/tests/src/Kernel/KnowledgeDefaultFormatterCacheTagsTest.php
@@ -3,8 +3,8 @@
 namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Core\Cache\Cache;
-use Drupal\entity_test\Entity\EntityTest;
 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
diff --git a/tests/src/Kernel/KnowledgeFieldAccessTest.php b/tests/src/Kernel/KnowledgeFieldAccessTest.php
index 5fe1ea4ece626bf1f6ae99fe506d39dee6a64b09..770defe676a227f7aa7ce8b42eab10fbb566548f 100644
--- a/tests/src/Kernel/KnowledgeFieldAccessTest.php
+++ b/tests/src/Kernel/KnowledgeFieldAccessTest.php
@@ -4,14 +4,14 @@ namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Session\AnonymousUserSession;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
-use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\knowledge\KnowledgeInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
 
diff --git a/tests/src/Kernel/KnowledgeIntegrationTest.php b/tests/src/Kernel/KnowledgeIntegrationTest.php
index 928e830b2cb3cd13edd009c637fa7992335d2766..e6d817cfd721686aa2f6de691c1d5fda358d74bc 100644
--- a/tests/src/Kernel/KnowledgeIntegrationTest.php
+++ b/tests/src/Kernel/KnowledgeIntegrationTest.php
@@ -5,12 +5,12 @@ namespace Drupal\Tests\knowledge\Kernel;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\Entity\Entity\EntityViewMode;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\KernelTests\KernelTestBase;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
-use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
  * Tests integration of knowledge with other components.
@@ -49,7 +49,6 @@ class KnowledgeIntegrationTest extends KernelTestBase {
     $this->installEntitySchema('user');
     $this->installEntitySchema('knowledge');
     $this->installSchema('dblog', ['watchdog']);
-    $this->installSchema('system', ['sequences']);
 
     // Create a new 'knowledge' knowledge-type.
     KnowledgeType::create([
diff --git a/tests/src/Kernel/KnowledgeItemTest.php b/tests/src/Kernel/KnowledgeItemTest.php
index 596c9b58aaf0f1f34a899520d9c2a9fd3b5ed256..19971ce6e275053ea9c2064a5bd3305aba63b0d3 100644
--- a/tests/src/Kernel/KnowledgeItemTest.php
+++ b/tests/src/Kernel/KnowledgeItemTest.php
@@ -3,11 +3,11 @@
 namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Tests\field\Kernel\FieldKernelTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
-use Drupal\Tests\field\Kernel\FieldKernelTestBase;
 
 /**
  * Tests the new entity API for the knowledge field type.
diff --git a/tests/src/Kernel/KnowledgeOrphanTest.php b/tests/src/Kernel/KnowledgeOrphanTest.php
index 35c2697ee755a32fc4f3a2efbc8a6ee95ca569fa..eb454c20b250f93a3e01b1efedf23920fa7b79cf 100644
--- a/tests/src/Kernel/KnowledgeOrphanTest.php
+++ b/tests/src/Kernel/KnowledgeOrphanTest.php
@@ -3,9 +3,9 @@
 namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\Tests\EntityViewTrait;
+use Drupal\field\Entity\FieldStorageConfig;
 
 /**
  * Tests loading and rendering orphan knowledge.
diff --git a/tests/src/Kernel/KnowledgeStringIdEntitiesTest.php b/tests/src/Kernel/KnowledgeStringIdEntitiesTest.php
index 9c8e0c6b301bdc57ebb6ec10877368e624c27a1f..d9995bcc652399075818dc8d560640dd3d449e2b 100644
--- a/tests/src/Kernel/KnowledgeStringIdEntitiesTest.php
+++ b/tests/src/Kernel/KnowledgeStringIdEntitiesTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\knowledge\Kernel;
 
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\knowledge\Entity\KnowledgeType;
 
 /**
diff --git a/tests/src/Kernel/KnowledgeUninstallTest.php b/tests/src/Kernel/KnowledgeUninstallTest.php
index 3564952d4d765e1c232a1c33ab4960d06b0d5279..7a5f29a2a02a408904232c830c388d3d807a7d53 100644
--- a/tests/src/Kernel/KnowledgeUninstallTest.php
+++ b/tests/src/Kernel/KnowledgeUninstallTest.php
@@ -3,8 +3,8 @@
 namespace Drupal\Tests\knowledge\Kernel;
 
 use Drupal\Core\Extension\ModuleUninstallValidatorException;
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
 
diff --git a/tests/src/Kernel/Migrate/MigrateKnowledgeStubTest.php b/tests/src/Kernel/Migrate/MigrateKnowledgeStubTest.php
index 28bc55b17c237d541e72479610032b3d957b4a3e..a410f9094b6198e22f72f3d838398fc452dd991b 100644
--- a/tests/src/Kernel/Migrate/MigrateKnowledgeStubTest.php
+++ b/tests/src/Kernel/Migrate/MigrateKnowledgeStubTest.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\Tests\knowledge\Kernel\Migrate;
 
+use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\migrate_drupal\Tests\StubTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
 
 /**
  * Test stub creation for knowledge entities.
@@ -28,7 +28,6 @@ class MigrateKnowledgeStubTest extends MigrateDrupalTestBase {
     parent::setUp();
     $this->installEntitySchema('knowledge');
     $this->installEntitySchema('node');
-    $this->installSchema('system', ['sequences']);
 
     // Make sure uid 0 is created (default uid for knowledge is 0).
     $storage = \Drupal::entityTypeManager()->getStorage('user');
diff --git a/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php b/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php
index c3edcddaae7fde1ef3b3a0d17012dc1735924751..39d49393666b198ab1d29dd813576b1670714ef7 100644
--- a/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php
+++ b/tests/src/Kernel/Views/FilterAndArgumentUserUidTest.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\knowledge\Kernel\Views;
 
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\node\Traits\NodeCreationTrait;
-use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\views\Tests\ViewResultAssertionTrait;
 use Drupal\views\Tests\ViewTestData;
 use Drupal\views\Views;
@@ -57,7 +57,6 @@ class FilterAndArgumentUserUidTest extends KernelTestBase {
    */
   public function testHandlers() {
     $this->installEntitySchema('user');
-    $this->installSchema('system', ['sequences']);
     $this->installEntitySchema('node');
     $this->installEntitySchema('knowledge');
     $this->installSchema('knowledge', ['knowledge_entity_statistics']);
diff --git a/tests/src/Kernel/Views/KnowledgeAdminViewTest.php b/tests/src/Kernel/Views/KnowledgeAdminViewTest.php
index c5892fbf046fc711108a8562b08e7817ad50a356..0d6a42d60b379bcaae52bcf0696eb896bf0b2cde 100644
--- a/tests/src/Kernel/Views/KnowledgeAdminViewTest.php
+++ b/tests/src/Kernel/Views/KnowledgeAdminViewTest.php
@@ -2,11 +2,11 @@
 
 namespace Drupal\Tests\knowledge\Kernel\Views;
 
+use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Entity\KnowledgeType;
 use Drupal\language\Entity\ConfigurableLanguage;
-use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
 use Drupal\views\Views;
diff --git a/tests/src/Kernel/Views/KnowledgeFieldNameTest.php b/tests/src/Kernel/Views/KnowledgeFieldNameTest.php
index a55eb359112f84b3f48badb017021eeecb75e25e..05f8ca46dcf9c57ebf87a2d7f0e2d734eecad0c8 100644
--- a/tests/src/Kernel/Views/KnowledgeFieldNameTest.php
+++ b/tests/src/Kernel/Views/KnowledgeFieldNameTest.php
@@ -3,13 +3,13 @@
 namespace Drupal\Tests\knowledge\Kernel\Views;
 
 use Drupal\Core\Render\RenderContext;
-use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\knowledge\Entity\Knowledge;
 use Drupal\knowledge\Tests\KnowledgeTestTrait;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\node\Traits\NodeCreationTrait;
-use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\views\Tests\ViewResultAssertionTrait;
 use Drupal\views\Tests\ViewTestData;
 use Drupal\views\Views;
@@ -63,7 +63,6 @@ class KnowledgeFieldNameTest extends KernelTestBase {
     $this->installEntitySchema('user');
     $this->installEntitySchema('node');
     $this->installEntitySchema('knowledge');
-    $this->installSchema('system', ['sequences']);
     $this->installSchema('knowledge', ['knowledge_entity_statistics']);
     $this->installConfig(['filter']);
 
diff --git a/tests/src/Kernel/Views/KnowledgeUserNameTest.php b/tests/src/Kernel/Views/KnowledgeUserNameTest.php
index 71c3a08d1a7d5b9a395d85e2bef6795c132c19b3..5cb59ec961a8a97137fe55e30739082a896eca00 100644
--- a/tests/src/Kernel/Views/KnowledgeUserNameTest.php
+++ b/tests/src/Kernel/Views/KnowledgeUserNameTest.php
@@ -3,9 +3,9 @@
 namespace Drupal\Tests\knowledge\Kernel\Views;
 
 use Drupal\Core\Session\AnonymousUserSession;
+use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
-use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
 use Drupal\views\Entity\View;
diff --git a/tests/src/Kernel/Views/KnowledgeViewsFieldAccessTest.php b/tests/src/Kernel/Views/KnowledgeViewsFieldAccessTest.php
index 1ab86e91659801ef5c338725f10820e4ad5996ff..1c004a971af3d5dbc5ba1b991a74727b90936da6 100644
--- a/tests/src/Kernel/Views/KnowledgeViewsFieldAccessTest.php
+++ b/tests/src/Kernel/Views/KnowledgeViewsFieldAccessTest.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\Tests\knowledge\Kernel\Views;
 
+use Drupal\Tests\views\Kernel\Handler\FieldFieldAccessTestBase;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\knowledge\Entity\Knowledge;
-use Drupal\Tests\views\Kernel\Handler\FieldFieldAccessTestBase;
 use Drupal\user\Entity\User;
 
 /**
diff --git a/tests/src/Unit/CompetencyAccessControlHandlerTest.php b/tests/src/Unit/CompetencyAccessControlHandlerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9c080553deda62c3cecdd93981138fac94728127
--- /dev/null
+++ b/tests/src/Unit/CompetencyAccessControlHandlerTest.php
@@ -0,0 +1,579 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\knowledge\Unit;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\CompetencyAccessControlHandler;
+use Drupal\user\UserInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * Tests the Competency access control handler.
+ *
+ * @coversDefaultClass \Drupal\knowledge\CompetencyAccessControlHandler
+ *   CompetencyAccessControlHandler
+ * @group knowledge
+ */
+class CompetencyAccessControlHandlerTest extends UnitTestCase {
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityType;
+
+  /**
+   * The competency access control handler.
+   *
+   * @var \Drupal\knowledge\CompetencyAccessControlHandler
+   */
+  protected $accessHandler;
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $cacheContextsManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entityType = $this->createMock('Drupal\Core\Entity\EntityTypeInterface');
+
+    $this->container = new ContainerBuilder();
+
+    $this->moduleHandler = $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->container->set('module_handler', $this->moduleHandler);
+
+    // cache_contexts_manager.
+    $this->cacheContextsManager = $this->createMock('Drupal\Core\Cache\Context\CacheContextsManager');
+    $this->container->set('cache_contexts_manager', $this->cacheContextsManager);
+
+    \Drupal::setContainer($this->container);
+
+    $this->accessHandler = new CompetencyAccessControlHandler($this->entityType);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessViewOwn(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->exactly(2))
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(1);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->exactly(2))
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $field_item],
+        ['knowledge_leader', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(13);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['view own knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwnerId')
+      ->willReturn(13);
+    $competency->expects($this->exactly(2))
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'view', $account], []],
+        ['knowledge_competency_access', [$competency, 'view', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'view', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessViewAny(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->exactly(2))
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(15);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->exactly(2))
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $field_item],
+        ['knowledge_leader', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(10);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['view any knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwnerId')
+      ->willReturn(1);
+    $competency->expects($this->exactly(2))
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'view', $account], []],
+        ['knowledge_competency_access', [$competency, 'view', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'view', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessViewLearner(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(10);
+
+    $coach_field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $coach_field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(3);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->exactly(2))
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $coach_field_item],
+        ['knowledge_leader', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(3);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['view learner knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwnerId')
+      ->willReturn(1);
+    $competency->expects($this->exactly(2))
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'view', $account], []],
+        ['knowledge_competency_access', [$competency, 'view', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'view', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessViewFollower(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(1);
+
+    $coach_field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $coach_field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(10);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->exactly(2))
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $coach_field_item],
+        ['knowledge_leader', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->expects($this->exactly(5))
+      ->method('id')
+      ->willReturn(1);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['view follower knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwnerId')
+      ->willReturn(9);
+    $competency->expects($this->exactly(2))
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'view', $account], []],
+        ['knowledge_competency_access', [$competency, 'view', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'view', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessDelete(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(1);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['administer knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'delete', $account], []],
+        ['knowledge_competency_access', [$competency, 'delete', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'delete', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testUnknownOperation(): void {
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(1);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'unknown', $account], []],
+        ['knowledge_competency_access', [$competency, 'unknown', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'unknown', $account);
+
+    $this->assertNotNull($result);
+    $this->assertFalse($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessUpdate(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(1);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->once())
+      ->method('id')
+      ->willReturn(3);
+    $owner->expects($this->once())
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(1);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['edit other knowledge_competency', TRUE],
+        ['edit learner knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'update', $account], []],
+        ['knowledge_competency_access', [$competency, 'update', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'update', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkAccess
+   */
+  public function testCheckAccessUpdateOwn(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $field_item = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
+    $field_item->expects($this->once())
+      ->method('__get')
+      ->with('target_id')
+      ->willReturn(1);
+
+    $owner = $this->createMock(UserInterface::class);
+    $owner->expects($this->once())
+      ->method('id')
+      ->willReturn(10);
+    $owner->expects($this->once())
+      ->method('get')
+      ->willReturnMap([
+        ['knowledge_coach', $field_item],
+      ]);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(10);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['edit own knowledge_competency', TRUE],
+      ]);
+
+    $language = $this->createMock('Drupal\Core\Language\LanguageInterface');
+    $language->expects($this->once())
+      ->method('getId')
+      ->willReturn('en');
+
+    $competency = $this->createMock('Drupal\knowledge\KnowledgeCompetencyInterface');
+    $competency->expects($this->once())
+      ->method('language')
+      ->willReturn($language);
+    $competency->expects($this->exactly(2))
+      ->method('getEntityTypeId')
+      ->willReturn('knowledge_competency');
+
+    $competency->expects($this->once())
+      ->method('getOwner')
+      ->willReturn($owner);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturnMap([
+        ['entity_access', [$competency, 'update', $account], []],
+        ['knowledge_competency_access', [$competency, 'update', $account], []],
+      ]);
+
+    $result = $this->accessHandler->access($competency, 'update', $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+  /**
+   * @covers ::checkCreateAccess
+   */
+  public function testCreateAccess(): void {
+    $this->cacheContextsManager->expects($this->exactly(2))
+      ->method('assertValidTokens')
+      ->with(['user.permissions'])
+      ->willReturn(TRUE);
+
+    $account = $this->createMock(AccountInterface::class);
+    $account->method('id')
+      ->willReturn(1);
+    $account->method('hasPermission')
+      ->willReturnMap([
+        ['add knowledge_competency', TRUE],
+      ]);
+
+    $this->moduleHandler->expects($this->exactly(2))
+      ->method('invokeAll')
+      ->willReturn([]);
+
+    $result = $this->accessHandler->createAccess(NULL, $account);
+
+    $this->assertNotNull($result);
+    $this->assertTrue($result);
+  }
+
+}
diff --git a/tests/src/Unit/CompetencyHtmlRouteProviderTest.php b/tests/src/Unit/CompetencyHtmlRouteProviderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf03e1f00fdfc898e3a90057894e62b48415641c
--- /dev/null
+++ b/tests/src/Unit/CompetencyHtmlRouteProviderTest.php
@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\knowledge\Unit;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\CompetencyHtmlRouteProvider;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @coversDefaultClass \Drupal\knowledge\CompetencyHtmlRouteProvider
+ * @group knowledge
+ */
+class CompetencyHtmlRouteProviderTest extends UnitTestCase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The competency HTML route provider.
+   *
+   * @var \Drupal\knowledge\CompetencyHtmlRouteProvider
+   */
+  protected $competencyHtmlRouteProvider;
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+
+    // $this->stringTranslation = $this->getStringTranslationStub();
+    // $this->container->set('string_translation', $this->stringTranslation);
+    $this->entityTypeManager = $this->createMock('\Drupal\Core\Entity\EntityTypeManagerInterface');
+    $this->container->set('entity_type.manager', $this->entityTypeManager);
+
+    $this->entityFieldManager = $this->createMock('\Drupal\Core\Entity\EntityFieldManagerInterface');
+    $this->container->set('entity_field.manager', $this->entityFieldManager);
+
+    \Drupal::setContainer($this->container);
+
+    $this->competencyHtmlRouteProvider = new CompetencyHtmlRouteProvider($this->entityTypeManager, $this->entityFieldManager);
+  }
+
+  /**
+   * Tests the getRoutes method.
+   *
+   * @covers ::getRoutes
+   * @covers ::getHistoryRoute
+   * @covers ::getRevisionRoute
+   * @covers ::getRevisionRevertRoute
+   * @covers ::getRevisionDeleteRoute
+   * @covers ::getSettingsFormRoute
+   * @covers ::getRoleFormRoute
+   */
+  public function testGetRoutes(): void {
+
+    // EntityTypeInterface $entity_type.
+    $entity_type = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
+    $entity_type->expects($this->exactly(5))
+      ->method('id')
+      ->willReturn('knowledge_competency');
+    $entity_type->expects($this->exactly(3))
+      ->method('getAdminPermission')
+      ->willReturn('administer knowledge_competency');
+    $entity_type->expects($this->exactly(11))
+      ->method('hasLinkTemplate')
+      ->willReturnMap([
+        ['add-form', TRUE],
+        ['canonical', TRUE],
+        ['edit-form', TRUE],
+        ['version-history', TRUE],
+        ['delete-form', TRUE],
+        ['collection', TRUE],
+        ['delete-multiple-form', FALSE],
+        ['revision', TRUE],
+        ['revision_revert', TRUE],
+        ['revision_delete', TRUE],
+      ]);
+    $entity_type->expects($this->exactly(7))
+      ->method('getLinkTemplate')
+      ->willReturnMap([
+        ['add-form', '/admin/content/knowledge/competency/add'],
+        ['canonical', '/admin/content/knowledge/competency/{knowledge_competency}'],
+        ['version-history', '/admin/content/knowledge/competency/{knowledge_competency}/revisions'],
+        ['edit-form', '/admin/content/knowledge/competency/{knowledge_competency}/edit'],
+        ['delete-form', '/admin/content/knowledge/competency/{knowledge_competency}/delete'],
+        ['collection', '/admin/content/knowledge/competency'],
+        [
+          'revision',
+          '/admin/content/knowledge/competency/{knowledge_competency}/revisions/{knowledge_competency_revision}/view',
+        ],
+        [
+          'revision_revert',
+          '/admin/content/knowledge/competency/{knowledge_competency}/revisions/{knowledge_competency_revision}/revert',
+        ],
+        [
+          'revision_delete',
+          '/admin/content/knowledge/competency/{knowledge_competency}/revisions/{knowledge_competency_revision}/delete',
+        ],
+      ]);
+
+    $routes = $this->competencyHtmlRouteProvider->getRoutes($entity_type);
+
+    $this->assertNotNull($routes);
+    $this->assertInstanceOf(RouteCollection::class, $routes);
+
+  }
+
+  /**
+   * Tests the getRoutes method when some routes are NULL.
+   *
+   * @covers ::getRoutes
+   * @covers ::getHistoryRoute
+   * @covers ::getRevisionRoute
+   * @covers ::getRevisionRevertRoute
+   * @covers ::getRevisionDeleteRoute
+   * @covers ::getSettingsFormRoute
+   * @covers ::getRoleFormRoute
+   */
+  public function testGetRoutesNotHas(): void {
+
+    // EntityTypeInterface $entity_type.
+    $entity_type = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
+    $entity_type->expects($this->exactly(5))
+      ->method('id')
+      ->willReturn('knowledge_competency');
+    $entity_type->expects($this->exactly(3))
+      ->method('getAdminPermission')
+      ->willReturn('administer knowledge_competency');
+    $entity_type->expects($this->exactly(11))
+      ->method('hasLinkTemplate')
+      ->willReturnMap([
+        ['add-form', TRUE],
+        ['canonical', TRUE],
+        ['edit-form', TRUE],
+        ['version-history', FALSE],
+        ['delete-form', TRUE],
+        ['collection', TRUE],
+        ['delete-multiple-form', FALSE],
+        ['revision', FALSE],
+        ['revision_revert', FALSE],
+        ['revision_delete', FALSE],
+      ]);
+    $entity_type->expects($this->exactly(3))
+      ->method('getLinkTemplate')
+      ->willReturnMap([
+        ['add-form', '/admin/content/knowledge/competency/add'],
+        ['canonical', '/admin/content/knowledge/competency/{knowledge_competency}'],
+        ['edit-form', '/admin/content/knowledge/competency/{knowledge_competency}/edit'],
+        ['delete-form', '/admin/content/knowledge/competency/{knowledge_competency}/delete'],
+        ['collection', '/admin/content/knowledge/competency'],
+      ]);
+
+    $routes = $this->competencyHtmlRouteProvider->getRoutes($entity_type);
+
+    $this->assertNotNull($routes);
+    $this->assertInstanceOf(RouteCollection::class, $routes);
+
+  }
+
+}
diff --git a/tests/src/Unit/CompetencyListBuilderTest.php b/tests/src/Unit/CompetencyListBuilderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..82ffd53df81d4b9cfbd934f320cbc7ea87fc5fce
--- /dev/null
+++ b/tests/src/Unit/CompetencyListBuilderTest.php
@@ -0,0 +1,201 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\knowledge\Unit;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Routing\RedirectDestinationInterface;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\CompetencyListBuilder;
+
+/**
+ * @coversDefaultClass \Drupal\knowledge\CompetencyListBuilder
+ * @group knowledge
+ */
+class CompetencyListBuilderTest extends UnitTestCase {
+
+  /**
+   * The entity type used for testing.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityType;
+
+  /**
+   * The module handler used for testing.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The translation manager used for testing.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $translationManager;
+
+  /**
+   * The competency storage used for testing.
+   *
+   * @var \Drupal\knowledge\KnowledgeCompetencyStorageInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $competencyStorage;
+
+  /**
+   * The service container used for testing.
+   *
+   * @var \Drupal\Core\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * The entity used to construct the EntityListBuilder.
+   *
+   * @var \Drupal\user\UserInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $user;
+
+  /**
+   * The redirect destination service.
+   *
+   * @var \Drupal\Core\Routing\RedirectDestinationInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $redirectDestination;
+
+  /**
+   * The EntityListBuilder object to test.
+   *
+   * @var \Drupal\knowledge\CompetencyListBuilder
+   */
+  protected $competencyListBuilder;
+
+  /**
+   * The string translation service.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   */
+  protected $stringTranslation;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->user = $this->createMock('Drupal\user\UserInterface');
+    $this->stringTranslation = $this->getStringTranslationStub();
+    $this->competencyStorage = $this->createMock('\Drupal\knowledge\KnowledgeCompetencyStorageInterface');
+    $this->moduleHandler = $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->entityType = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
+    $this->translationManager = $this->createMock('\Drupal\Core\StringTranslation\TranslationInterface');
+    $this->competencyListBuilder = new CompetencyListBuilder($this->entityType, $this->competencyStorage);
+    $this->redirectDestination = $this->createMock(RedirectDestinationInterface::class);
+    $this->container = new ContainerBuilder();
+    $this->container->set('module_handler', $this->moduleHandler);
+    $this->container->set('string_translation', $this->stringTranslation);
+    \Drupal::setContainer($this->container);
+  }
+
+  /**
+   * @covers ::buildHeader
+   */
+  public function testBuildHeader(): void {
+    $header = $this->competencyListBuilder->buildHeader();
+    $this->assertIsArray($header);
+    $this->assertArrayHasKey('id', $header);
+    $this->assertArrayHasKey('user', $header);
+    $this->assertArrayHasKey('operations', $header);
+  }
+
+  /**
+   * @covers ::buildRow
+   */
+  public function testBuildRow() {
+    $entity = $this->createMock('\Drupal\knowledge\Entity\Competency');
+    $entity->expects($this->exactly(2))
+      ->method('id')
+      ->willReturn(1);
+    $entity->expects($this->once())
+      ->method('getOwner')
+      ->willReturn($this->user);
+    $this->user->expects($this->once())
+      ->method('label')
+      ->willReturn('test');
+    $url = Url::fromRoute('entity.knowledge_competency.edit_form', ['knowledge_competency' => 1]);
+
+    $operation_name = $this->randomMachineName();
+    $operations = [
+      $operation_name => [
+        'title' => $this->randomMachineName(),
+      ],
+    ];
+    $this->moduleHandler->expects($this->once())
+      ->method('invokeAll')
+      ->with('entity_operation', [$entity])
+      ->willReturn($operations);
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('entity_operation');
+
+    $row = $this->competencyListBuilder->buildRow($entity);
+    $this->assertIsArray($row);
+    $this->assertArrayHasKey('id', $row);
+    $this->assertArrayHasKey('user', $row);
+    $this->assertArrayHasKey('operations', $row);
+  }
+
+  /**
+   * @covers ::getOperations
+   */
+  public function testGetOperations(): void {
+    $operation_name = $this->randomMachineName();
+    $operations = [
+      $operation_name => [
+        'title' => $this->randomMachineName(),
+      ],
+    ];
+    $this->moduleHandler->expects($this->once())
+      ->method('invokeAll')
+      ->with('entity_operation', [$this->user])
+      ->willReturn($operations);
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('entity_operation');
+
+    $this->user->expects($this->any())
+      ->method('access')
+      ->willReturn(AccessResult::allowed());
+    $this->user->expects($this->any())
+      ->method('hasLinkTemplate')
+      ->willReturn(TRUE);
+    $url = Url::fromRoute('entity.user_role.collection');
+    $this->user->expects($this->any())
+      ->method('toUrl')
+      ->willReturn($url);
+
+    $this->redirectDestination->expects($this->atLeastOnce())
+      ->method('getAsArray')
+      ->willReturn(['destination' => '/foo/bar']);
+
+    $list = new CompetencyListBuilder($this->entityType, $this->competencyStorage);
+    $list->setStringTranslation($this->translationManager);
+    $list->setRedirectDestination($this->redirectDestination);
+
+    $operations = $list->getOperations($this->user);
+    $this->assertIsArray($operations);
+    $this->assertArrayHasKey('edit', $operations);
+    $this->assertIsArray($operations['edit']);
+    $this->assertArrayHasKey('title', $operations['edit']);
+    $this->assertArrayHasKey('delete', $operations);
+    $this->assertIsArray($operations['delete']);
+    $this->assertArrayHasKey('title', $operations['delete']);
+    $this->assertArrayHasKey($operation_name, $operations);
+    $this->assertIsArray($operations[$operation_name]);
+    $this->assertArrayHasKey('title', $operations[$operation_name]);
+  }
+
+}
diff --git a/tests/src/Unit/CompetencyStorageTest.php b/tests/src/Unit/CompetencyStorageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8aee8c892ddfd36ed3b11abff73afdf8ebb0914f
--- /dev/null
+++ b/tests/src/Unit/CompetencyStorageTest.php
@@ -0,0 +1,202 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\knowledge\Unit;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\CompetencyStorage;
+use Drupal\knowledge\KnowledgeCompetencyInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * @coversDefaultClass \Drupal\knowledge\CompetencyStorage
+ * @group knowledge
+ */
+class CompetencyStorageTest extends UnitTestCase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The competency storage.
+   *
+   * @var \Drupal\knowledge\CompetencyStorage
+   */
+  protected $competencyStorage;
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityType;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $database;
+
+  /**
+   * The cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $cache;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $languageManager;
+
+  /**
+   * The memory cache.
+   *
+   * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $memoryCache;
+
+  /**
+   * The entity type bundle manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityTypeBundleManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+
+    // $this->stringTranslation = $this->getStringTranslationStub();
+    // $this->container->set('string_translation', $this->stringTranslation);
+    $this->entityTypeManager = $this->createMock('\Drupal\Core\Entity\EntityTypeManager');
+    $this->container->set('entity_type.manager', $this->entityTypeManager);
+
+    $this->entityFieldManager = $this->createMock('\Drupal\Core\Entity\EntityFieldManager');
+    $this->container->set('entity_field.manager', $this->entityFieldManager);
+
+    $this->database = $this->createMock('\Drupal\Core\Database\Connection');
+    $this->container->set('database', $this->database);
+
+    $this->cache = $this->createMock('\Drupal\Core\Cache\CacheBackendInterface');
+    $this->languageManager = $this->createMock('\Drupal\Core\Language\LanguageManager');
+    $this->memoryCache = $this->createMock('\Drupal\Core\Cache\MemoryCache\MemoryCache');
+    $this->entityTypeBundleManager = $this->createMock('\Drupal\Core\Entity\EntityTypeBundleInfo');
+
+    \Drupal::setContainer($this->container);
+
+    $this->entityType = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
+
+  }
+
+  /**
+   * @covers ::revisionIds
+   */
+  public function testRevisionIds() {
+    $entity = $this->createMock(KnowledgeCompetencyInterface::class);
+    $entity->expects($this->once())
+      ->method('id')
+      ->willReturn(123);
+    $this->entityType->expects($this->exactly(3))
+      ->method('id')
+      ->willReturn('knowledge_competency');
+
+    $entity_type = $this->createMock('\Drupal\Core\Entity\ContentEntityTypeInterface');
+    $this->entityTypeManager->expects($this->once())
+      ->method('getActiveDefinition')
+      ->with('knowledge_competency')
+      ->willReturn($entity_type);
+
+    $this->entityFieldManager->expects($this->once())
+      ->method('getActiveFieldStorageDefinitions')
+      ->with('knowledge_competency')
+      ->willReturn([]);
+
+    $select_statement = $this->createMock('\Drupal\Core\Database\StatementInterface');
+    $this->database->expects($this->once())
+      ->method('query')
+      ->with('SELECT vid FROM {knowledge_competency_revision} WHERE id=:id ORDER BY vid', [':id' => 123])
+      ->willReturn($select_statement);
+
+    $competencyStorage = new CompetencyStorage(
+      $this->entityType,
+      $this->database,
+      $this->entityFieldManager,
+      $this->cache,
+      $this->languageManager,
+      $this->memoryCache,
+      $this->entityTypeBundleManager,
+      $this->entityTypeManager,
+    );
+    $competencyStorage->revisionIds($entity);
+  }
+
+  /**
+   * @covers ::userRevisionIds
+   */
+  public function testUserRevisionIds() {
+    $account = $this->createMock('\Drupal\Core\Session\AccountInterface');
+    $account->expects($this->once())
+      ->method('id')
+      ->willReturn(123);
+    $this->entityType->expects($this->exactly(3))
+      ->method('id')
+      ->willReturn('knowledge_competency');
+
+    $entity_type = $this->createMock('\Drupal\Core\Entity\ContentEntityTypeInterface');
+    $this->entityTypeManager->expects($this->once())
+      ->method('getActiveDefinition')
+      ->with('knowledge_competency')
+      ->willReturn($entity_type);
+
+    $this->entityFieldManager->expects($this->once())
+      ->method('getActiveFieldStorageDefinitions')
+      ->with('knowledge_competency')
+      ->willReturn([]);
+
+    $select_statement = $this->createMock('\Drupal\Core\Database\StatementInterface');
+    $this->database->expects($this->once())
+      ->method('query')
+      ->with('SELECT vid FROM {knowledge_competency_field_revision} WHERE uid = :uid ORDER BY vid', [':uid' => 123])
+      ->willReturn($select_statement);
+
+    $competencyStorage = new CompetencyStorage(
+      $this->entityType,
+      $this->database,
+      $this->entityFieldManager,
+      $this->cache,
+      $this->languageManager,
+      $this->memoryCache,
+      $this->entityTypeBundleManager,
+      $this->entityTypeManager,
+    );
+    $competencyStorage->userRevisionIds($account);
+  }
+
+}
diff --git a/tests/src/Unit/KnowledgeLinkBuilderTest.php b/tests/src/Unit/KnowledgeLinkBuilderTest.php
index 5e8ba4098b5d88be46fe1c66f68bf803f9dc0f64..937a6be93b73336fcb067d9a7ea09c80473b04e6 100644
--- a/tests/src/Unit/KnowledgeLinkBuilderTest.php
+++ b/tests/src/Unit/KnowledgeLinkBuilderTest.php
@@ -4,11 +4,10 @@ namespace Drupal\Tests\knowledge\Unit;
 
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
 use Drupal\knowledge\KnowledgeLinkBuilder;
 use Drupal\knowledge\Plugin\Field\FieldType\KnowledgeItemInterface;
 use Drupal\node\NodeInterface;
-use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
-use Drupal\Tests\UnitTestCase;
 
 /**
  * @coversDefaultClass \Drupal\knowledge\KnowledgeLinkBuilder
@@ -16,8 +15,6 @@ use Drupal\Tests\UnitTestCase;
  */
 class KnowledgeLinkBuilderTest extends UnitTestCase {
 
-  use GeneratePermutationsTrait;
-
   /**
    * Knowledge manager mock.
    *
diff --git a/tests/src/Unit/KnowledgeManagerTest.php b/tests/src/Unit/KnowledgeManagerTest.php
index a17800d1331022a29e2a62fc3f540939ae9e1b42..1bf7da6af3928fa33f2cc2a25e42e60d37a3f114 100644
--- a/tests/src/Unit/KnowledgeManagerTest.php
+++ b/tests/src/Unit/KnowledgeManagerTest.php
@@ -7,8 +7,8 @@ use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\knowledge\KnowledgeManager;
 use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\KnowledgeManager;
 use Prophecy\PhpUnit\ProphecyTrait;
 use Prophecy\Prophet;
 
diff --git a/tests/src/Unit/KnowledgeStatisticsUnitTest.php b/tests/src/Unit/KnowledgeStatisticsUnitTest.php
index 926f55d35aa5cdfd337e1d78129a083b18c3fa40..feb4144b8c60e218475e1a0ae6fec4c1d2766f2c 100644
--- a/tests/src/Unit/KnowledgeStatisticsUnitTest.php
+++ b/tests/src/Unit/KnowledgeStatisticsUnitTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\knowledge\Unit;
 
-use Drupal\knowledge\KnowledgeStatistics;
 use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\KnowledgeStatistics;
 
 /**
  * @coversDefaultClass \Drupal\knowledge\KnowledgeStatistics
diff --git a/tests/src/Unit/Plugin/views/field/KnowledgeBulkFormTest.php b/tests/src/Unit/Plugin/views/field/KnowledgeBulkFormTest.php
index 8b0280d981da0f8807adab70f0d7c1dfd75e15e1..de5bccb490e89e8dc06876a2a1a915df75767514 100644
--- a/tests/src/Unit/Plugin/views/field/KnowledgeBulkFormTest.php
+++ b/tests/src/Unit/Plugin/views/field/KnowledgeBulkFormTest.php
@@ -5,8 +5,8 @@ namespace Drupal\Tests\knowledge\Unit\Plugin\views\field;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\knowledge\Plugin\views\field\KnowledgeBulkForm;
 use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\Plugin\views\field\KnowledgeBulkForm;
 
 /**
  * @coversDefaultClass \Drupal\knowledge\Plugin\views\field\KnowledgeBulkForm
@@ -67,6 +67,10 @@ class KnowledgeBulkFormTest extends UnitTestCase {
       ->will($this->returnValue(['table' => ['entity type' => 'knowledge']]));
     $container = new ContainerBuilder();
     $container->set('views.views_data', $views_data);
+
+    $route_match = $this->createMock('Drupal\Core\Routing\ResettableStackedRouteMatchInterface');
+    $container->set('current_route_match', $route_match);
+
     $container->set('string_translation', $this->getStringTranslationStub());
     \Drupal::setContainer($container);
 
@@ -84,7 +88,7 @@ class KnowledgeBulkFormTest extends UnitTestCase {
     $definition['title'] = '';
     $options = [];
 
-    $knowledge_bulk_form = new KnowledgeBulkForm([], 'knowledge_bulk_form', $definition, $entity_type_manager, $language_manager, $messenger, $entity_repository);
+    $knowledge_bulk_form = new KnowledgeBulkForm([], 'knowledge_bulk_form', $definition, $entity_type_manager, $language_manager, $messenger, $entity_repository, $route_match);
     $knowledge_bulk_form->init($executable, $display, $options);
 
     $reflected_actions = (new \ReflectionObject($knowledge_bulk_form))->getProperty('actions');
diff --git a/tests/src/Unit/Service/CompetencyServiceTest.php b/tests/src/Unit/Service/CompetencyServiceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d880bc0f5cf083fd2e32f5a2c63df3cc13430cd
--- /dev/null
+++ b/tests/src/Unit/Service/CompetencyServiceTest.php
@@ -0,0 +1,419 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\knowledge\Service\Unit;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\knowledge\Service\CompetencyService;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * @coversDefaultClass \Drupal\knowledge\Service\CompetencyService
+ * @group knowledge
+ */
+class CompetencyServiceTest extends UnitTestCase {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $configFactory;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
+   * The string translation service.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $stringTranslation;
+
+  /**
+   * The messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $messenger;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setup(): void {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+
+    $this->configFactory = $this->createMock('\Drupal\Core\Config\ConfigFactoryInterface');
+    $this->container->set('config.factory', $this->configFactory);
+
+    $this->entityTypeManager = $this->createMock('\Drupal\Core\Entity\EntityTypeManagerInterface');
+    $this->container->set('entity_type.manager', $this->entityTypeManager);
+
+    $this->stringTranslation = $this->getStringTranslationStub();
+    $this->container->set('string_translation', $this->stringTranslation);
+
+    $this->messenger = $this->createMock('\Drupal\Core\Messenger\MessengerInterface');
+    $this->container->set('messenger', $this->messenger);
+
+    \Drupal::setContainer($this->container);
+  }
+
+  /**
+   * Test get method.
+   *
+   * @covers ::get
+   * @covers ::sort
+   */
+  public function testGet() {
+    $config = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
+    $config->expects($this->once())
+      ->method('get')
+      ->with('roles')
+      ->willReturn([
+        [
+          'role' => 'knowledge_contributor',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_publisher',
+          'weight' => 2,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_canidate',
+          'weight' => 0,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+      ]);
+
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('knowledge.competency.settings')
+      ->willReturn($config);
+
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+
+    $expected = [
+      [
+        'role' => 'knowledge_canidate',
+        'weight' => 0,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_contributor',
+        'weight' => 1,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_publisher',
+        'weight' => 2,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+    ];
+    $this->assertEquals($expected, $service->get());
+  }
+
+  /**
+   * Test get method.
+   *
+   * @covers ::get
+   * @covers ::sort
+   */
+  public function testGetSameWeight() {
+    $config = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
+    $config->expects($this->once())
+      ->method('get')
+      ->with('roles')
+      ->willReturn([
+        [
+          'role' => 'knowledge_contributor',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_publisher',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_canidate',
+          'weight' => 0,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+      ]);
+
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('knowledge.competency.settings')
+      ->willReturn($config);
+
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+
+    $expected = [
+      [
+        'role' => 'knowledge_canidate',
+        'weight' => 0,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_contributor',
+        'weight' => 1,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+      [
+        'role' => 'knowledge_publisher',
+        'weight' => 1,
+        'action' => 'auto',
+        'promote' => 'self',
+      ],
+    ];
+    $this->assertEquals($expected, $service->get());
+  }
+
+  /**
+   * Test __construct method.
+   *
+   * @covers ::__construct
+   */
+  public function testConstruct() {
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+    $this->assertInstanceOf(CompetencyService::class, $service);
+  }
+
+  /**
+   * Test getRoleIds method.
+   *
+   * @covers ::getRoleIds
+   */
+  public function testGetRoleIds(): void {
+    $config = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
+    $config->expects($this->once())
+      ->method('get')
+      ->with('roles')
+      ->willReturn([
+        [
+          'role' => 'knowledge_contributor',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_publisher',
+          'weight' => 2,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_canidate',
+          'weight' => 0,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+      ]);
+
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('knowledge.competency.settings')
+      ->willReturn($config);
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+    $this->assertEquals(['knowledge_canidate', 'knowledge_contributor', 'knowledge_publisher'], $service->getRoleIds());
+  }
+
+  /**
+   * Test hasRoleOrBotter method.
+   *
+   * @covers ::hasRoleOrBetter
+   */
+  public function testHasRoleOrBetter(): void {
+    $config = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
+    $config->expects($this->exactly(3))
+      ->method('get')
+      ->with('roles')
+      ->willReturn([
+        [
+          'role' => 'knowledge_contributor',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_publisher',
+          'weight' => 2,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_canidate',
+          'weight' => 0,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+      ]);
+
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('knowledge.competency.settings')
+      ->willReturn($config);
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+
+    $user_roles = ['authenticated', 'knowledge_contributor'];
+    $this->assertTrue($service->hasRoleOrBetter('knowledge_canidate', $user_roles));
+    $this->assertTrue($service->hasRoleOrBetter('knowledge_contributor', $user_roles));
+    $this->assertFalse($service->hasRoleOrBetter('knowledge_publisher', $user_roles));
+    $this->assertFalse($service->hasRoleOrBetter('knowledge_botter', $user_roles));
+  }
+
+  /**
+   * Test getUserCompetency method.
+   *
+   * @covers ::getUserCompetency
+   */
+  public function testGetUserCompetencyNew(): void {
+    $query = $this->createMock('\Drupal\Core\Entity\Query\QueryInterface');
+    $query->expects($this->once())
+      ->method('condition')
+      ->with('user_id', 1)
+      ->willReturnSelf();
+    $query->expects($this->once())
+      ->method('accessCheck')
+      ->with(FALSE)
+      ->willReturnSelf();
+    $query->expects($this->once())
+      ->method('execute')
+      ->willReturn([]);
+
+    $role_storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
+
+    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
+    $storage->expects($this->once())
+      ->method('getQuery')
+      ->willReturn($query);
+    $storage->expects($this->once())
+      ->method('create')
+      ->with(['user_id' => 1])
+      ->willReturn('competency');
+
+    $this->entityTypeManager->expects($this->exactly(2))
+      ->method('getStorage')
+      ->willReturnMap([
+        ['knowledge_competency', $storage],
+        ['user_role', $role_storage],
+      ]);
+
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+    $this->assertEquals('competency', $service->getUserCompetency(1));
+  }
+
+  /**
+   * Test getUserCompetency method.
+   *
+   * @covers ::getUserCompetency
+   */
+  public function testGetUserCompetencyExisting(): void {
+    $query = $this->createMock('\Drupal\Core\Entity\Query\QueryInterface');
+    $query->expects($this->once())
+      ->method('condition')
+      ->with('user_id', 1)
+      ->willReturnSelf();
+    $query->expects($this->once())
+      ->method('accessCheck')
+      ->with(FALSE)
+      ->willReturnSelf();
+    $query->expects($this->once())
+      ->method('execute')
+      ->willReturn([1]);
+    $role_storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
+
+    $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface');
+    $storage->expects($this->once())
+      ->method('getQuery')
+      ->willReturn($query);
+    $storage->expects($this->once())
+      ->method('load')
+      ->with(1)
+      ->willReturn('competency');
+
+    $this->entityTypeManager->expects($this->exactly(2))
+      ->method('getStorage')
+      ->willReturnMap([
+          ['knowledge_competency', $storage],
+          ['user_role', $role_storage],
+      ]);
+
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+    $this->assertEquals('competency', $service->getUserCompetency(1));
+  }
+
+  /**
+   * Tests doRoleRemoval method.
+   *
+   * @covers ::doRoleRemoval
+   * @covers ::getRemovableRoles
+   */
+  public function testDoRoleRemoval(): void {
+    $config = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
+    $config->expects($this->once())
+      ->method('get')
+      ->with('roles')
+      ->willReturn([
+        [
+          'role' => 'knowledge_contributor',
+          'weight' => 1,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_publisher',
+          'weight' => 2,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+        [
+          'role' => 'knowledge_canidate',
+          'weight' => 0,
+          'action' => 'auto',
+          'promote' => 'self',
+        ],
+      ]);
+
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('knowledge.competency.settings')
+      ->willReturn($config);
+
+    $service = new CompetencyService($this->configFactory, $this->entityTypeManager, $this->stringTranslation, $this->messenger);
+    $user = $this->createMock('\Drupal\user\UserInterface');
+    $user->expects($this->once())
+      ->method('getRoles')
+      ->willReturn(['knowledge_contributor', 'knowledge_publisher']);
+
+    $service->doRoleRemoval($user);
+  }
+
+}