diff --git a/templates/apply.yml b/templates/apply.yml
index 3836302c773311e28cf9696710a0c6cb2bdc2421..fc1546fed377580c24381adda14ed34262fece95 100644
--- a/templates/apply.yml
+++ b/templates/apply.yml
@@ -88,6 +88,13 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir. Only used if no_plan is true otherwise the variables are coming from the plan.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
 
 ---
 
@@ -97,10 +104,7 @@ spec:
     name: $TF_STATE_NAME
     action: start
   resource_group: $TF_STATE_NAME
-  rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_apply ]]" == "true"'
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-      when: manual
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/custom-command.yml b/templates/custom-command.yml
index 3fc702287b445b5edfd40cdaa39ae3474ff42427..49bb1712a7c02bff47b9ba5746e53cff42c3780f 100644
--- a/templates/custom-command.yml
+++ b/templates/custom-command.yml
@@ -74,11 +74,19 @@ spec:
     command:
       description: 'The gitlab-tofu command to run.'
 
+    needs:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `needs` of the job.'
+
 ---
 
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
-  needs: []
+  needs: $[[ inputs.needs ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/delete-state.yml b/templates/delete-state.yml
index 9a499ea54809cba25f334dd00abf7c68606087c6..d3f0c97f2156a8573ca93463340effb5ac906090 100644
--- a/templates/delete-state.yml
+++ b/templates/delete-state.yml
@@ -15,6 +15,13 @@ spec:
     create_delete_state_job:
       default: 'true'
       description: 'Wheather the delete-state job should be created or not.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
 
 ---
 
@@ -26,6 +33,4 @@ spec:
     TF_STATE_NAME: $[[ inputs.state_name ]]
   script:
     - curl --request DELETE -u "gitlab-ci-token:$CI_JOB_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/terraform/state/$TF_STATE_NAME"
-  rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-    - when: manual
+  rules: $[[ inputs.rules ]]
diff --git a/templates/destroy.yml b/templates/destroy.yml
index 07914954e875154e21d20741bb051efae8d2d838..bc1897756208a944932272e8cec18b544d12618c 100644
--- a/templates/destroy.yml
+++ b/templates/destroy.yml
@@ -88,6 +88,13 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir. Only used if no_plan is true otherwise the variables are coming from the plan.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
 
 ---
 
@@ -97,9 +104,7 @@ spec:
     name: $TF_STATE_NAME
     action: stop
   resource_group: $TF_STATE_NAME
-  rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_destroy ]]" == "true"'
-    - when: manual
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/fmt.yml b/templates/fmt.yml
index b732794edcb18a5a50f1aa482098690fa33ed6f1..afaf8acbffca01c8a1c2915aae9519d3e00cda1b 100644
--- a/templates/fmt.yml
+++ b/templates/fmt.yml
@@ -76,16 +76,28 @@ spec:
       type: boolean
       description: 'If the job is allowed to fail or not.'
 
+    needs:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `needs` of the job.'
+
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
+
 ---
 
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
-  needs: []
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-    - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
-      when: never
-    - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+  needs: $[[ inputs.needs ]]
+  rules: $[[ inputs.rules ]]
   allow_failure: $[[ inputs.allow_failure ]]
   cache:
     key: "$__CACHE_KEY_HACK"
diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml
index 260d6e36b08382652a8a6ef67596b717aef71a76..38aabd5f56920b58a33b439a54acb707b2f45cd3 100644
--- a/templates/full-pipeline.yml
+++ b/templates/full-pipeline.yml
@@ -100,6 +100,57 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    fmt_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `fmt` job.'
+    validate_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `validate` job.'
+    test_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `test` job.'
+    plan_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `plan` job.'
+    apply_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_apply ]]" == "true"'
+        - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+          when: manual
+      type: array
+      description: 'Defines the `rules` of the `apply` job.'
+    destroy_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_destroy ]]" == "true"'
+        - when: manual
+      type: array
+      description: 'Defines the `rules` of the `destroy` job.'
+    delete_state_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+        - when: manual
+      type: array
+      description: 'Defines the `rules` of the `delete-state` job.'
 
 ---
 
@@ -115,6 +166,8 @@ include:
       image_name: $[[ inputs.image_name ]]
       image_digest: $[[ inputs.image_digest ]]
       root_dir: $[[ inputs.root_dir ]]
+      needs: []
+      rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
@@ -128,6 +181,7 @@ include:
       root_dir: $[[ inputs.root_dir ]]
       state_name: $[[ inputs.state_name ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.validate_rules ]]
   - local: '/templates/test.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]test'
@@ -141,6 +195,8 @@ include:
       root_dir: $[[ inputs.root_dir ]]
       state_name: $[[ inputs.state_name ]]
       var_file: $[[ inputs.var_file ]]
+      needs: []
+      rules: $[[ inputs.test_rules ]]
     rules:
       - exists:
           - $[[ inputs.root_dir ]]/**/*.tftest.hcl
@@ -158,6 +214,7 @@ include:
       state_name: $[[ inputs.state_name ]]
       artifacts_access: $[[ inputs.plan_artifacts_access ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.plan_rules ]]
   - local: '/templates/apply.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
@@ -172,6 +229,7 @@ include:
       state_name: $[[ inputs.state_name ]]
       auto_apply: $[[ inputs.auto_apply ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.apply_rules ]]
   - local: '/templates/destroy.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
@@ -186,12 +244,17 @@ include:
       state_name: $[[ inputs.state_name ]]
       auto_destroy: $[[ inputs.auto_destroy ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.destroy_rules ]]
   - local: '/templates/delete-state.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
       stage: $[[ inputs.stage_cleanup ]]
       state_name: $[[ inputs.state_name ]]
+      rules: $[[ inputs.delete_state_rules ]]
 
-# NOTE: we have to define this `needs` here, because inputs don't support arrays, yet.
+# FIXME: eventually, we'll have a `needs` input on the `delete-state`
+# job template, but the issue is that we cannot default it to something
+# meaningful other than `null` - but `null` is also not yet supported, see
+# https://gitlab.com/gitlab-org/gitlab/-/issues/440468
 $[[ inputs.job_name_prefix ]]delete-state:
   needs: ['$[[ inputs.job_name_prefix ]]destroy']
diff --git a/templates/graph.yml b/templates/graph.yml
index 18cc1bf450e18f22b89b9e4cc74fb231fb4d4294..27b0296809d5cf89627c096cf5d48432ebcdc052 100644
--- a/templates/graph.yml
+++ b/templates/graph.yml
@@ -82,12 +82,19 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    needs:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `needs` of the job.'
 
 ---
 
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
-  needs: []
+  needs: $[[ inputs.needs ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/plan.yml b/templates/plan.yml
index d6af1e8d863660e9c71a648e416572e7570b6f1e..33972b587846282ba41cf933e9001ab1fcf763da 100644
--- a/templates/plan.yml
+++ b/templates/plan.yml
@@ -87,6 +87,13 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
 
 ---
 
@@ -106,11 +113,7 @@ spec:
       - $TF_ROOT/$[[ inputs.plan_name ]].cache
     reports:
       terraform: $TF_ROOT/$[[ inputs.plan_name]].json
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-    - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
-      when: never
-    - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/test.yml b/templates/test.yml
index 376f2c4e73c395ff2b3d6669fe6f0da40b12d07c..729acea15074f423afa6a3b85ab6969085b2d544 100644
--- a/templates/test.yml
+++ b/templates/test.yml
@@ -77,17 +77,27 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    needs:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `needs` of the job.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
 
 ---
 
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
-  needs: []
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-    - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
-      when: never
-    - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+  needs: $[[ inputs.needs ]]
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/templates/validate-plan-apply.yml b/templates/validate-plan-apply.yml
index 9b3e65f4fa8658515ab6f9fca7561a2cf3e952c2..0c7be75f84f303482a9b9546c96a3b3fcfd9e171 100644
--- a/templates/validate-plan-apply.yml
+++ b/templates/validate-plan-apply.yml
@@ -90,6 +90,37 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    fmt_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `fmt` job.'
+    validate_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `validate` job.'
+    plan_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `plan` job.'
+    apply_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_apply ]]" == "true"'
+        - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+          when: manual
+      type: array
+      description: 'Defines the `rules` of the `apply` job.'
 
 ---
 
@@ -105,6 +136,8 @@ include:
       image_name: $[[ inputs.image_name ]]
       image_digest: $[[ inputs.image_digest ]]
       root_dir: $[[ inputs.root_dir ]]
+      needs: []
+      rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
@@ -118,6 +151,7 @@ include:
       root_dir: $[[ inputs.root_dir ]]
       state_name: $[[ inputs.state_name ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.validate_rules ]]
   - local: '/templates/plan.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
@@ -132,6 +166,7 @@ include:
       state_name: $[[ inputs.state_name ]]
       artifacts_access: $[[ inputs.plan_artifacts_access ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.plan_rules ]]
   - local: '/templates/apply.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
@@ -146,3 +181,4 @@ include:
       state_name: $[[ inputs.state_name ]]
       auto_apply: $[[ inputs.auto_apply ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.apply_rules ]]
diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml
index d64a448a72361ee08c2e714a1ad4d6d61efee169..ebd35b4e436b7b6adfa4068c5e2fbfe853193fe0 100644
--- a/templates/validate-plan-destroy.yml
+++ b/templates/validate-plan-destroy.yml
@@ -93,6 +93,42 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    fmt_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `fmt` job.'
+    validate_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `validate` job.'
+    plan_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `plan` job.'
+    destroy_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_destroy ]]" == "true"'
+        - when: manual
+      type: array
+      description: 'Defines the `rules` of the `destroy` job.'
+    delete_state_rules:
+      default:
+        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+        - when: manual
+      type: array
+      description: 'Defines the `rules` of the `delete-state` job.'
 
 ---
 
@@ -108,6 +144,8 @@ include:
       image_name: $[[ inputs.image_name ]]
       image_digest: $[[ inputs.image_digest ]]
       root_dir: $[[ inputs.root_dir ]]
+      needs: []
+      rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
@@ -121,6 +159,7 @@ include:
       root_dir: $[[ inputs.root_dir ]]
       state_name: $[[ inputs.state_name ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.validate_rules ]]
   - local: '/templates/plan.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
@@ -137,6 +176,7 @@ include:
       artifacts_access: $[[ inputs.plan_artifacts_access ]]
       destroy: true
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.plan_rules ]]
   - local: '/templates/destroy.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
@@ -153,12 +193,17 @@ include:
       plan_name: $[[ inputs.plan_name ]]
       auto_destroy: $[[ inputs.auto_destroy ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.destroy_rules ]]
   - local: '/templates/delete-state.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
       stage: $[[ inputs.stage_cleanup ]]
       state_name: $[[ inputs.state_name ]]
+      rules: $[[ inputs.delete_state_rules ]]
 
-# NOTE: we have to define this `needs` here, because inputs don't support arrays, yet.
+# FIXME: eventually, we'll have a `needs` input on the `delete-state`
+# job template, but the issue is that we cannot default it to something
+# meaningful other than `null` - but `null` is also not yet supported, see
+# https://gitlab.com/gitlab-org/gitlab/-/issues/440468
 $[[ inputs.job_name_prefix ]]delete-state:
   needs: ['$[[ inputs.job_name_prefix ]]destroy']
diff --git a/templates/validate-plan.yml b/templates/validate-plan.yml
index 39b61dd9bc63923ef6450c9da374277cd4f00b33..235bf6f6debf93c4957a278b03d620ea7f22b27a 100644
--- a/templates/validate-plan.yml
+++ b/templates/validate-plan.yml
@@ -83,6 +83,30 @@ spec:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
+    fmt_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `fmt` job.'
+    validate_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `validate` job.'
+    plan_rules:
+      default:
+        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+        - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
+          when: never
+        - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+      type: array
+      description: 'Defines the `rules` of the `plan` job.'
 
 ---
 
@@ -98,6 +122,8 @@ include:
       image_name: $[[ inputs.image_name ]]
       image_digest: $[[ inputs.image_digest ]]
       root_dir: $[[ inputs.root_dir ]]
+      needs: []
+      rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
@@ -111,6 +137,7 @@ include:
       root_dir: $[[ inputs.root_dir ]]
       state_name: $[[ inputs.state_name ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.validate_rules ]]
   - local: '/templates/plan.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
@@ -125,3 +152,4 @@ include:
       state_name: $[[ inputs.state_name ]]
       artifacts_access: $[[ inputs.artifacts_access ]]
       var_file: $[[ inputs.var_file ]]
+      rules: $[[ inputs.plan_rules ]]
diff --git a/templates/validate.yml b/templates/validate.yml
index 9b474e24897ac175bfea616f05f2fb5e599a6ad5..fdb431c7ded06e7bbeafa211c6e8a9e96aaff235 100644
--- a/templates/validate.yml
+++ b/templates/validate.yml
@@ -70,23 +70,29 @@ spec:
     root_dir:
       default: ${CI_PROJECT_DIR}
       description: 'Root directory for the OpenTofu project.'
+
     state_name:
       default: default
       description: 'Remote OpenTofu state name.'
+
     var_file:
       default: ''
       type: string
       description: 'Path to a variables files relative to root_dir.'
 
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: []
+      type: array
+      description: 'Defines the `rules` of the job.'
+
 ---
 
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
-    - if: $CI_OPEN_MERGE_REQUESTS  # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
-      when: never
-    - if: $CI_COMMIT_BRANCH        # If there's no open merge request, add it to a *branch* pipeline instead.
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     paths:
diff --git a/tests/integration-tests/Defaults.gitlab-ci.yml b/tests/integration-tests/Defaults.gitlab-ci.yml
index 09762e0d0e81c605c21a819db2dff78494815e71..2c51ac10e9ddc19edc825cc2e78c1001f83b406b 100644
--- a/tests/integration-tests/Defaults.gitlab-ci.yml
+++ b/tests/integration-tests/Defaults.gitlab-ci.yml
@@ -7,29 +7,13 @@ include:
       opentofu_version: $OPENTOFU_VERSION
       root_dir: $TEST_TF_ROOT
       state_name: $TEST_TF_STATE_NAME
+      # Required to run everything immediately, instead of manually.
+      fmt_rules: [{when: always}]
+      validate_rules: [{when: always}]
+      test_rules: [{when: always}]
+      plan_rules: [{when: always}]
+      apply_rules: [{when: always}]
+      destroy_rules: [{when: always}]
+      delete_state_rules: [{when: always}]
 
 stages: [validate, test, build, deploy, cleanup]
-
-# Required to run everything immediately, instead of manually.
-
-fmt:
-  rules: [{when: always}]
-
-validate:
-  rules: [{when: always}]
-
-test:
-  rules: [{when: always}]
-
-plan:
-  rules: [{when: always}]
-
-apply:
-  rules: [{when: always}]
-
-destroy:
-  rules: [{when: always}]
-
-delete-state:
-  rules: [{when: always}]
-
diff --git a/tests/integration-tests/Destroy.gitlab-ci.yml b/tests/integration-tests/Destroy.gitlab-ci.yml
index d0ec3c56ae349723dedf34cdfd13d4561621ec08..874168e8b3e24c256f1eaa70c1a982bc7b85bc7b 100644
--- a/tests/integration-tests/Destroy.gitlab-ci.yml
+++ b/tests/integration-tests/Destroy.gitlab-ci.yml
@@ -10,6 +10,8 @@ include:
       state_name: $TEST_TF_STATE_NAME
       no_plan: true
       auto_apply: true
+      # Required to run everything immediately, instead of manually.
+      rules: [{when: always}]
 
   - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/validate-plan-destroy@$CI_COMMIT_SHA
     inputs:
@@ -18,26 +20,11 @@ include:
       opentofu_version: $OPENTOFU_VERSION
       root_dir: $TEST_TF_ROOT
       state_name: $TEST_TF_STATE_NAME
+      # Required to run everything immediately, instead of manually.
+      fmt_rules: [{when: always}]
+      validate_rules: [{when: always}]
+      plan_rules: [{when: always}]
+      destroy_rules: [{when: always}]
+      delete_state_rules: [{when: always}]
 
 stages: [setup, validate, build, cleanup]
-
-# Required to run everything immediately, instead of manually.
-
-'setup:apply':
-  rules: [{when: always}]
-
-fmt:
-  rules: [{when: always}]
-
-validate:
-  rules: [{when: always}]
-
-plan:
-  rules: [{when: always}]
-
-destroy:
-  rules: [{when: always}]
-
-delete-state:
-  rules: [{when: always}]
-
diff --git a/tests/integration-tests/TestJob.gitlab-ci.yml b/tests/integration-tests/TestJob.gitlab-ci.yml
index adf8da87f9d9cb09ea32a1725c42763335cce558..96e90eecd7990cf5976bb604ddd2fa901961f60d 100644
--- a/tests/integration-tests/TestJob.gitlab-ci.yml
+++ b/tests/integration-tests/TestJob.gitlab-ci.yml
@@ -6,10 +6,7 @@ include:
       opentofu_version: $OPENTOFU_VERSION
       root_dir: $TEST_TF_ROOT
       state_name: $TEST_TF_STATE_NAME
+      # Required to run everything immediately, instead of manually.
+      rules: [{when: always}]
 
 stages: [test]
-
-# Required to run everything immediately, instead of manually.
-
-test:
-  rules: [{when: always}]
diff --git a/tests/integration-tests/VarFile.gitlab-ci.yml b/tests/integration-tests/VarFile.gitlab-ci.yml
index 0c035dae6f4d7ab226aa7453dbba4a6340da3927..f1505f31e66bcf391008879f7dfa3d19c9522c8f 100644
--- a/tests/integration-tests/VarFile.gitlab-ci.yml
+++ b/tests/integration-tests/VarFile.gitlab-ci.yml
@@ -8,29 +8,13 @@ include:
       root_dir: $TEST_TF_ROOT
       state_name: $TEST_TF_STATE_NAME
       var_file: varfile.integration-test.tfvars
+      # Required to run everything immediately, instead of manually.
+      fmt_rules: [{when: always}]
+      validate_rules: [{when: always}]
+      test_rules: [{when: always}]
+      plan_rules: [{when: always}]
+      apply_rules: [{when: always}]
+      destroy_rules: [{when: always}]
+      delete_state_rules: [{when: always}]
 
 stages: [validate, test, build, deploy, cleanup]
-
-# Required to run everything immediately, instead of manually.
-
-fmt:
-  rules: [{when: always}]
-
-validate:
-  rules: [{when: always}]
-
-test:
-  rules: [{when: always}]
-
-plan:
-  rules: [{when: always}]
-
-apply:
-  rules: [{when: always}]
-
-destroy:
-  rules: [{when: always}]
-
-delete-state:
-  rules: [{when: always}]
-