diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d30619e0c0dddf8f3d4dae6fb628dbcdfc9ae17e..0d8c836d6bf12914246f7f9412dfa1feddd57083 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,10 @@
+workflow:
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+    - if: $CI_PIPELINE_SOURCE == "schedule"
+    - if: $CI_COMMIT_TAG
+    - if: $CI_COMMIT_REF_PROTECTED == "true"
+
 include: 
   - local: tests/unit.gitlab-ci.yml
   - local: tests/integration.gitlab-ci.yml
diff --git a/.gitlab/README.md b/.gitlab/README.md
index 571b01348e79f607e1396eadf19448248d6786df..b16477ee5295d408b841b12de32ea1a59609b1f4 100644
--- a/.gitlab/README.md
+++ b/.gitlab/README.md
@@ -30,7 +30,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@<VERSION>
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: <VERSION>
       opentofu_version: <OPENTOFU_VERSION>
@@ -46,7 +46,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@~latest
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: latest
       opentofu_version: 1.6.0
@@ -60,7 +60,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@0.0.0-alpha1
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: 0.0.0-alpha1
       opentofu_version: 1.6.0
@@ -68,6 +68,34 @@ include:
 stages: [validate, test, build, deploy, cleanup]
 ```
 
+Instead of including the `full-pipeline`, it's also possible to include individual jobs
+and compose your own pipeline, for example, to just run the `fmt` job you can do:
+
+```yaml
+include:
+  - component: gitlab.com/components/opentofu/fmt@latest
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: latest
+      opentofu_version: 1.6.0
+      root_dir: tofu/
+```
+
+Have a look at the [`full-pipeline`](templates/full-pipeline.yml) for how it's constructed.
+
+The following job components exist:
+
+- [`fmt`](templates/fmt.yml)
+- [`validate`](templates/validate.yml)
+- [`plan`](templates/plan.yml)
+- [`apply`](templates/apply.yml)
+- [`destroy`](templates/destroy.yml)
+- [`delete-state`](templates/delete-state.yml)
+
+Have a look at the individual template spec to learn about the available inputs.
+
 ### Inputs
 
 | Name | Default | Description |
diff --git a/Makefile b/Makefile
index 44619cdaf25ad9abb3edcf3d03800050cd4d369b..a764c3921511aa7c3c4023a2d4985491c17b9145 100644
--- a/Makefile
+++ b/Makefile
@@ -29,26 +29,17 @@ backports:
 	@echo "Generating $(BACKPORTS_BASE_FILE) ..."
 	@mkdir -p $(BACKPORTS_BASE_DIR)
 	@cp $(BACKPORTS_DIR)/.Base.gitlab-ci.yml $(BACKPORTS_BASE_FILE)
-	@sed '1,/^---$$/d' templates/full-pipeline.yml >> $(BACKPORTS_BASE_FILE)
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.stage_validate \]\]/validate/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.stage_build \]\]/build/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.stage_deploy \]\]/deploy/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.stage_cleanup \]\]/cleanup/'
+	@sed '1,/^---$$/d' templates/fmt.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:fmt/' | sed -e 's/$$\[\[ inputs.stage \]\]/validate/' | sed -e 's/$$\[\[ inputs.allow_failure \]\]/true/' >> $(BACKPORTS_BASE_FILE)
+	@sed '1,/^---$$/d' templates/validate.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:validate/' | sed -e 's/$$\[\[ inputs.stage \]\]/validate/' >> $(BACKPORTS_BASE_FILE)
+	@sed '1,/^---$$/d' templates/plan.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:plan/' | sed -e 's/$$\[\[ inputs.stage \]\]/build/' >> $(BACKPORTS_BASE_FILE)
+	@sed '1,/^---$$/d' templates/apply.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:apply/' | sed -e 's/$$\[\[ inputs.stage \]\]/deploy/' | sed -e 's/$$\[\[ inputs.auto_apply \]\]/$$_TF_AUTO_APPLY/' >> $(BACKPORTS_BASE_FILE)
+	@sed '1,/^---$$/d' templates/destroy.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:destroy/' | sed -e 's/$$\[\[ inputs.stage \]\]/cleanup/' | sed -e 's/$$\[\[ inputs.auto_destroy \]\]/$$_TF_AUTO_DESTROY/' | sed -e 's/$$\[\[ inputs.create_destroy_job \]\]/$$TF_CREATE_DESTROY_JOB/' >> $(BACKPORTS_BASE_FILE)
+	@sed '1,/^---$$/d' templates/delete-state.yml | sed -e 's/$$\[\[ inputs.as \]\]/.opentofu:delete-state/' | sed -e 's/$$\[\[ inputs.stage \]\]/cleanup/' | sed -e 's/$$\[\[ inputs.create_delete_state_job \]\]/$$TF_CREATE_DELETE_STATE_JOB/' | sed -e "/stage: cleanup/a \ \ needs: ['.opentofu:destroy']" >> $(BACKPORTS_BASE_FILE)
+	
+	@# Common inputs
 	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.image_registry_base \]\]/$$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/'
 	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.version \]\]/$$GITLAB_OPENTOFU_VERSION/'
 	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.opentofu_version \]\]/$$OPENTOFU_VERSION/'
 	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.root_dir \]\]/$$TF_ROOT/'
 	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.state_name \]\]/$$TF_STATE_NAME/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.auto_apply \]\]/$$TF_AUTO_APPLY/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.auto_destroy \]\]/$$TF_AUTO_DESTROY/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.create_destroy_job \]\]/$$TF_CREATE_DESTROY_JOB/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/$$\[\[ inputs.create_delete_state_job \]\]/$$TF_CREATE_DELETE_STATE_JOB/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/\.default/.opentofu:default/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/^fmt:$$/.opentofu:fmt:/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/^validate:$$/.opentofu:validate:/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/^plan:$$/.opentofu:plan:/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/^apply:$$/.opentofu:apply:/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/destroy:/.opentofu:destroy:/'
-	@sed -i $(BACKPORTS_BASE_FILE) -e '/needs: \[destroy\]/d'
-	@sed -i $(BACKPORTS_BASE_FILE) -e 's/^delete-state:$$/.opentofu:delete-state:/'
 	@echo "Generated $(BACKPORTS_BASE_FILE)"
diff --git a/README.md b/README.md
index 99768d1440ce7f5a204854e34dd292fc80082c06..74e64f97bd609ec991e1c190ce6fa01247a215e4 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@<VERSION>
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: <VERSION>
       opentofu_version: <OPENTOFU_VERSION>
@@ -48,7 +48,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@~latest
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: latest
       opentofu_version: 1.6.0
@@ -62,7 +62,7 @@ include:
   - component: gitlab.com/components/opentofu/full-pipeline@0.0.0-alpha1
     inputs:
       # The version must currently be specified explicitly as an input,
-      # to find the correctly associated images. # This can be removed 
+      # to find the correctly associated images. # This can be removed
       # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
       version: 0.0.0-alpha1
       opentofu_version: 1.6.0
@@ -70,6 +70,34 @@ include:
 stages: [validate, test, build, deploy, cleanup]
 ```
 
+Instead of including the `full-pipeline`, it's also possible to include individual jobs
+and compose your own pipeline, for example, to just run the `fmt` job you can do:
+
+```yaml
+include:
+  - component: gitlab.com/components/opentofu/fmt@latest
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: latest
+      opentofu_version: 1.6.0
+      root_dir: tofu/
+```
+
+Have a look at the [`full-pipeline`](templates/full-pipeline.yml) for how it's constructed.
+
+The following job components exist:
+
+- [`fmt`](templates/fmt.yml)
+- [`validate`](templates/validate.yml)
+- [`plan`](templates/plan.yml)
+- [`apply`](templates/apply.yml)
+- [`destroy`](templates/destroy.yml)
+- [`delete-state`](templates/delete-state.yml)
+
+Have a look at the individual template spec to learn about the available inputs.
+
 ### Inputs
 
 | Name | Default | Description |
diff --git a/backports/.Base.gitlab-ci.yml b/backports/.Base.gitlab-ci.yml
index 2484a6effbe989e3c245ef1298f455c235398c29..e601d06db2806e88f6290a3fa48f1d5ec84e8225 100644
--- a/backports/.Base.gitlab-ci.yml
+++ b/backports/.Base.gitlab-ci.yml
@@ -45,7 +45,7 @@ variables:
       echo "      version: latest"
       echo "      opentofu_version: 1.6.0"
       echo ""
-      echo "stages: [validate, test, build, deploy]"
+      echo "stages: [validate, build, deploy, cleanup]"
       echo " "
       echo "You can read about more about the OpenTofu CI/CD component here:"
       echo "https://gitlab.com/components/opentofu"
diff --git a/backports/OpenTofu/Base.gitlab-ci.yml b/backports/OpenTofu/Base.gitlab-ci.yml
index 2be22ab19bba082c610786b5669bd4563ffa28ba..5f035b9d5f25316f60d0b028976bf1b28ceba2e7 100644
--- a/backports/OpenTofu/Base.gitlab-ci.yml
+++ b/backports/OpenTofu/Base.gitlab-ci.yml
@@ -45,55 +45,54 @@ variables:
       echo "      version: latest"
       echo "      opentofu_version: 1.6.0"
       echo ""
-      echo "stages: [validate, test, build, deploy]"
+      echo "stages: [validate, build, deploy, cleanup]"
       echo " "
       echo "You can read about more about the OpenTofu CI/CD component here:"
       echo "https://gitlab.com/components/opentofu"
     - 'false'
 
-.opentofu:default:
-  image:
-    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
-
-  variables:
-    TF_ROOT: $TF_ROOT
-    TF_STATE_NAME: $TF_STATE_NAME
-
-  cache:
-    key: "$TF_ROOT"
-    paths:
-      - $TF_ROOT/.terraform/
-
-.opentofu:fmt:
-  extends: .opentofu:default
+'.opentofu:fmt':
   stage: validate
   needs: []
-  script:
-    - gitlab-tofu fmt
-  allow_failure: true
   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.
+  #allow_failure: true
+  allow_failure: true
+  cache:
+    key: "$TF_ROOT"
+    paths:
+      - $TF_ROOT/.terraform/
+  variables:
+    TF_ROOT: $TF_ROOT
+  image:
+    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
+  script:
+    - gitlab-tofu fmt
 
-.opentofu:validate:
-  extends: .opentofu:default
+'.opentofu:validate':
   stage: validate
-  script:
-    - gitlab-tofu validate
   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.
+  cache:
+    key: "$TF_ROOT"
+    paths:
+      - $TF_ROOT/.terraform/
+  variables:
+    TF_ROOT: $TF_ROOT
+    TF_STATE_NAME: $TF_STATE_NAME
+  image:
+    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
+  script:
+    - gitlab-tofu validate
 
-.opentofu:plan:
-  extends: .opentofu:default
+'.opentofu:plan':
   stage: build
-  script:
-    - gitlab-tofu plan
-    - gitlab-tofu plan-json
   environment: 
     name: $TF_STATE_NAME
     action: prepare
@@ -115,26 +114,43 @@ variables:
     - 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.
+  cache:
+    key: "$TF_ROOT"
+    paths:
+      - $TF_ROOT/.terraform/
+  variables:
+    TF_ROOT: $TF_ROOT
+    TF_STATE_NAME: $TF_STATE_NAME
+  image:
+    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
+  script:
+    - gitlab-tofu plan
+    - gitlab-tofu plan-json
 
-.opentofu:apply:
-  extends: .opentofu:default
+'.opentofu:apply':
   stage: deploy
-  script:
-    - gitlab-tofu apply
   environment:
     name: $TF_STATE_NAME
     action: start
   resource_group: $TF_STATE_NAME
   rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$TF_AUTO_APPLY" == "true"'
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$_TF_AUTO_APPLY" == "true"'
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
       when: manual
+  cache:
+    key: "$TF_ROOT"
+    paths:
+      - $TF_ROOT/.terraform/
+  variables:
+    TF_ROOT: $TF_ROOT
+    TF_STATE_NAME: $TF_STATE_NAME
+  image:
+    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
+  script:
+    - gitlab-tofu apply
 
-.opentofu:destroy:
-  extends: .opentofu:default
+'.opentofu:destroy':
   stage: cleanup
-  script:
-    - gitlab-tofu destroy
   environment:
     name: $TF_STATE_NAME
     action: stop
@@ -142,13 +158,25 @@ variables:
   rules:
     - if: '"$TF_CREATE_DESTROY_JOB" != "true"'
       when: never
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$TF_AUTO_DESTROY" == "true"'
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$_TF_AUTO_DESTROY" == "true"'
     - when: manual
+  cache:
+    key: "$TF_ROOT"
+    paths:
+      - $TF_ROOT/.terraform/
+  variables:
+    TF_ROOT: $TF_ROOT
+    TF_STATE_NAME: $TF_STATE_NAME
+  image:
+    name: '$GITLAB_OPENTOFU_IMAGE_REGISTRY_BASE/gitlab-opentofu:$GITLAB_OPENTOFU_VERSION-opentofu$OPENTOFU_VERSION'
+  script:
+    - gitlab-tofu destroy
 
-.opentofu:delete-state:
-  extends: .opentofu:default
+'.opentofu:delete-state':
   stage: cleanup
+  needs: ['.opentofu:destroy']
   resource_group: $TF_STATE_NAME
+  image: curlimages/curl:latest
   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:
@@ -156,4 +184,3 @@ variables:
       when: never
     - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
     - when: manual
-
diff --git a/templates/apply.yml b/templates/apply.yml
new file mode 100644
index 0000000000000000000000000000000000000000..01316f382a32e4fd6ba6664acb54644fe54cf2e4
--- /dev/null
+++ b/templates/apply.yml
@@ -0,0 +1,70 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'apply'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'deploy'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Versions
+    # This version is only required, because we cannot access the context of the component,
+    # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    version:
+      default: 'latest'
+      description: 'Version of this component. Has to be the same as the one in the component include entry.'
+      
+    opentofu_version:
+      default: '1.6.0'
+      options:
+        - '$OPENTOFU_VERSION'
+        - '1.6.0'
+        - '1.6.0-rc1'
+      description: 'OpenTofu version that should be used.'
+
+    # Images
+    image_registry_base:
+      default: '$CI_REGISTRY/components/opentofu'
+    # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722
+    # gitlab_opentofu_image:
+    #   # FIXME: This should reference the component tag that is used.
+    #   #        Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    #   # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]'
+    #   default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+    #   description: 'Tag of the gitlab-opentofu image.'
+    
+    # Configuration
+    root_dir: 
+      default: ${CI_PROJECT_DIR}
+      description: 'Root directory for the OpenTofu project.'
+    state_name:
+      default: default
+      description: 'Remote OpenTofu state name.'
+    auto_apply:
+      default: 'false'
+      description: 'Whether the apply job is manual or automatically run.'
+
+---
+
+'$[[ inputs.as ]]':
+  stage: $[[ inputs.stage ]]
+  environment:
+    name: $[[ inputs.state_name ]]
+    action: start
+  resource_group: $[[ inputs.state_name ]]
+  rules:
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_apply ]]" == "true"'
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+      when: manual
+  cache:
+    key: "$[[ inputs.root_dir ]]"
+    paths:
+      - $[[ inputs.root_dir ]]/.terraform/
+  variables:
+    TF_ROOT: $[[ inputs.root_dir ]]
+    TF_STATE_NAME: $[[ inputs.state_name ]]
+  image:
+    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+  script:
+    - gitlab-tofu apply
diff --git a/templates/delete-state.yml b/templates/delete-state.yml
new file mode 100644
index 0000000000000000000000000000000000000000..300f299724f17e8f7ae56ef02e46dadcb26badbc
--- /dev/null
+++ b/templates/delete-state.yml
@@ -0,0 +1,31 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'delete-state'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'cleanup'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Configuration
+    state_name:
+      default: default
+      description: 'Remote OpenTofu state name.'
+    create_delete_state_job:
+      default: 'true'
+      description: 'Wheather the delete-state job should be created or not.'
+
+---
+
+'$[[ inputs.as ]]':
+  stage: $[[ inputs.stage ]]
+  resource_group: $[[ inputs.state_name ]]
+  image: curlimages/curl:latest
+  script:
+    - curl --request DELETE -u "gitlab-ci-token:$CI_JOB_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/terraform/state/$[[ inputs.state_name ]]"
+  rules:
+    - if: '"$[[ inputs.create_delete_state_job ]]" != "true"'
+      when: never
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+    - when: manual
diff --git a/templates/destroy.yml b/templates/destroy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b343dc17ada44172249e3fc7b0d3ea4fe9f623b
--- /dev/null
+++ b/templates/destroy.yml
@@ -0,0 +1,74 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'destroy'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'cleanup'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Versions
+    # This version is only required, because we cannot access the context of the component,
+    # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    version:
+      default: 'latest'
+      description: 'Version of this component. Has to be the same as the one in the component include entry.'
+      
+    opentofu_version:
+      default: '1.6.0'
+      options:
+        - '$OPENTOFU_VERSION'
+        - '1.6.0'
+        - '1.6.0-rc1'
+      description: 'OpenTofu version that should be used.'
+
+    # Images
+    image_registry_base:
+      default: '$CI_REGISTRY/components/opentofu'
+    # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722
+    # gitlab_opentofu_image:
+    #   # FIXME: This should reference the component tag that is used.
+    #   #        Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    #   # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]'
+    #   default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+    #   description: 'Tag of the gitlab-opentofu image.'
+    
+    # Configuration
+    root_dir: 
+      default: ${CI_PROJECT_DIR}
+      description: 'Root directory for the OpenTofu project.'
+    state_name:
+      default: default
+      description: 'Remote OpenTofu state name.'
+    auto_destroy:
+      default: 'false'
+      description: 'Whether the destroy job is manual or automatically run.'
+    create_destroy_job:
+      default: 'true'
+      description: 'Wheather the destroy job should be created or not.'
+
+---
+
+'$[[ inputs.as ]]':
+  stage: $[[ inputs.stage ]]
+  environment:
+    name: $[[ inputs.state_name ]]
+    action: stop
+  resource_group: $[[ inputs.state_name ]]
+  rules:
+    - if: '"$[[ inputs.create_destroy_job ]]" != "true"'
+      when: never
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_destroy ]]" == "true"'
+    - when: manual
+  cache:
+    key: "$[[ inputs.root_dir ]]"
+    paths:
+      - $[[ inputs.root_dir ]]/.terraform/
+  variables:
+    TF_ROOT: $[[ inputs.root_dir ]]
+    TF_STATE_NAME: $[[ inputs.state_name ]]
+  image:
+    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+  script:
+    - gitlab-tofu destroy
diff --git a/templates/fmt.yml b/templates/fmt.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aaea90d3211444d6711f210675bd731e6393034a
--- /dev/null
+++ b/templates/fmt.yml
@@ -0,0 +1,69 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'fmt'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'validate'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Versions
+    # This version is only required, because we cannot access the context of the component,
+    # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    version:
+      default: 'latest'
+      description: 'Version of this component. Has to be the same as the one in the component include entry.'
+      
+    opentofu_version:
+      default: '1.6.0'
+      options:
+        - '$OPENTOFU_VERSION'
+        - '1.6.0'
+        - '1.6.0-rc1'
+      description: 'OpenTofu version that should be used.'
+
+    # Images
+    image_registry_base:
+      default: '$CI_REGISTRY/components/opentofu'
+    # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722
+    # gitlab_opentofu_image:
+    #   # FIXME: This should reference the component tag that is used.
+    #   #        Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    #   # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]'
+    #   default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+    #   description: 'Tag of the gitlab-opentofu image.'
+    
+    # Configuration
+    root_dir: 
+      default: ${CI_PROJECT_DIR}
+      description: 'Root directory for the OpenTofu project.'
+
+    # FIXME: wait for https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142009
+    # allow_failure:
+    #   default: true
+    #   type: boolean
+    #   description: 'If the job is allowed to fail or not.'
+
+---
+
+'$[[ 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.
+  #allow_failure: $[[ inputs.allow_failure ]]
+  allow_failure: true
+  cache:
+    key: "$[[ inputs.root_dir ]]"
+    paths:
+      - $[[ inputs.root_dir ]]/.terraform/
+  variables:
+    TF_ROOT: $[[ inputs.root_dir ]]
+  image:
+    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+  script:
+    - gitlab-tofu fmt
diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml
index 297ecada6d098f5834489cb2c2147c0a6f034be9..efe637a994254ef1959514d95ce5375aaa192849 100644
--- a/templates/full-pipeline.yml
+++ b/templates/full-pipeline.yml
@@ -62,110 +62,61 @@ spec:
 
 ---
 
-.default:
-  image:
-    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
-
-  variables:
-    TF_ROOT: $[[ inputs.root_dir ]]
-    TF_STATE_NAME: $[[ inputs.state_name ]]
-
-  cache:
-    key: "$[[ inputs.root_dir ]]"
-    paths:
-      - $[[ inputs.root_dir ]]/.terraform/
-
-fmt:
-  extends: .default
-  stage: $[[ inputs.stage_validate ]]
-  needs: []
-  script:
-    - gitlab-tofu fmt
-  allow_failure: true
-  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.
-
-validate:
-  extends: .default
-  stage: $[[ inputs.stage_validate ]]
-  script:
-    - gitlab-tofu validate
-  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.
-
-plan:
-  extends: .default
-  stage: $[[ inputs.stage_build ]]
-  script:
-    - gitlab-tofu plan
-    - gitlab-tofu plan-json
-  environment: 
-    name: $[[ inputs.state_name ]]
-    action: prepare
-  resource_group: $[[ inputs.state_name ]]
-  artifacts:
-    # Terraform's cache files can include secrets which can be accidentally exposed.
-    # Please exercise caution when utilizing secrets in your Terraform infrastructure and
-    # consider limiting access to artifacts or take other security measures to protect sensitive information.
-    #
-    # The next line, which disables public access to pipeline artifacts, is not available on GitLab.com.
-    # See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
-    public: false
-    paths:
-      - $[[ inputs.root_dir ]]/plan.cache
-    reports:
-      terraform: $[[ inputs.root_dir ]]/plan.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.
-
-apply:
-  extends: .default
-  stage: $[[ inputs.stage_deploy ]]
-  script:
-    - gitlab-tofu apply
-  environment:
-    name: $[[ inputs.state_name ]]
-    action: start
-  resource_group: $[[ inputs.state_name ]]
-  rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_apply ]]" == "true"'
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-      when: manual
-
-destroy:
-  extends: .default
-  stage: $[[ inputs.stage_cleanup ]]
-  script:
-    - gitlab-tofu destroy
-  environment:
-    name: $[[ inputs.state_name ]]
-    action: stop
-  resource_group: $[[ inputs.state_name ]]
-  rules:
-    - if: '"$[[ inputs.create_destroy_job ]]" != "true"'
-      when: never
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && "$[[ inputs.auto_destroy ]]" == "true"'
-    - when: manual
+include:
+  - local: '/templates/fmt.yml'
+    inputs:
+      as: 'fmt'
+      stage: $[[ inputs.stage_validate ]]
+      version: $[[ inputs.version ]]
+      opentofu_version: $[[ inputs.opentofu_version ]]
+      image_registry_base: $[[ inputs.image_registry_base ]]
+      root_dir: $[[ inputs.root_dir ]]
+  - local: '/templates/validate.yml'
+    inputs:
+      as: 'validate'
+      stage: $[[ inputs.stage_validate ]]
+      version: $[[ inputs.version ]]
+      opentofu_version: $[[ inputs.opentofu_version ]]
+      image_registry_base: $[[ inputs.image_registry_base ]]
+      root_dir: $[[ inputs.root_dir ]]
+      state_name: $[[ inputs.state_name ]]
+  - local: '/templates/plan.yml'
+    inputs:
+      as: 'plan'
+      stage: $[[ inputs.stage_build ]]
+      version: $[[ inputs.version ]]
+      opentofu_version: $[[ inputs.opentofu_version ]]
+      image_registry_base: $[[ inputs.image_registry_base ]]
+      root_dir: $[[ inputs.root_dir ]]
+      state_name: $[[ inputs.state_name ]]
+  - local: '/templates/apply.yml'
+    inputs:
+      as: 'apply'
+      stage: $[[ inputs.stage_deploy ]]
+      version: $[[ inputs.version ]]
+      opentofu_version: $[[ inputs.opentofu_version ]]
+      image_registry_base: $[[ inputs.image_registry_base ]]
+      root_dir: $[[ inputs.root_dir ]]
+      state_name: $[[ inputs.state_name ]]
+      auto_apply: $[[ inputs.auto_apply ]]
+  - local: '/templates/destroy.yml'
+    inputs:
+      as: 'destroy'
+      stage: $[[ inputs.stage_cleanup ]]
+      version: $[[ inputs.version ]]
+      opentofu_version: $[[ inputs.opentofu_version ]]
+      image_registry_base: $[[ inputs.image_registry_base ]]
+      root_dir: $[[ inputs.root_dir ]]
+      state_name: $[[ inputs.state_name ]]
+      auto_destroy: $[[ inputs.auto_apply ]]
+      create_destroy_job: $[[ inputs.create_destroy_job ]]
+  - local: '/templates/delete-state.yml'
+    inputs:
+      as: 'delete-state'
+      stage: $[[ inputs.stage_cleanup ]]
+      state_name: $[[ inputs.state_name ]]
+      create_delete_state_job: $[[ inputs.create_delete_state_job ]]
 
+# NOTE: we have to define this `needs` here, because inputs don't support arrays, yet.
 delete-state:
-  extends: .default
-  stage: $[[ inputs.stage_cleanup ]]
   needs: [destroy]
-  resource_group: $[[ inputs.state_name ]]
-  script:
-    - curl --request DELETE -u "gitlab-ci-token:$CI_JOB_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/terraform/state/$[[ inputs.state_name ]]"
-  rules:
-    - if: '"$[[ inputs.create_delete_state_job ]]" != "true"'
-      when: never
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-    - when: manual
-
diff --git a/templates/plan.yml b/templates/plan.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a2a618dbef5bb039c605a409a40edbdf54c5139b
--- /dev/null
+++ b/templates/plan.yml
@@ -0,0 +1,81 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'plan'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'build'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Versions
+    # This version is only required, because we cannot access the context of the component,
+    # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    version:
+      default: 'latest'
+      description: 'Version of this component. Has to be the same as the one in the component include entry.'
+      
+    opentofu_version:
+      default: '1.6.0'
+      options:
+        - '$OPENTOFU_VERSION'
+        - '1.6.0'
+        - '1.6.0-rc1'
+      description: 'OpenTofu version that should be used.'
+
+    # Images
+    image_registry_base:
+      default: '$CI_REGISTRY/components/opentofu'
+    # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722
+    # gitlab_opentofu_image:
+    #   # FIXME: This should reference the component tag that is used.
+    #   #        Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    #   # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]'
+    #   default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+    #   description: 'Tag of the gitlab-opentofu image.'
+    
+    # Configuration
+    root_dir: 
+      default: ${CI_PROJECT_DIR}
+      description: 'Root directory for the OpenTofu project.'
+    state_name:
+      default: default
+      description: 'Remote OpenTofu state name.'
+
+---
+
+'$[[ inputs.as ]]':
+  stage: $[[ inputs.stage ]]
+  environment: 
+    name: $[[ inputs.state_name ]]
+    action: prepare
+  resource_group: $[[ inputs.state_name ]]
+  artifacts:
+    # Terraform's cache files can include secrets which can be accidentally exposed.
+    # Please exercise caution when utilizing secrets in your Terraform infrastructure and
+    # consider limiting access to artifacts or take other security measures to protect sensitive information.
+    #
+    # The next line, which disables public access to pipeline artifacts, is not available on GitLab.com.
+    # See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
+    public: false
+    paths:
+      - $[[ inputs.root_dir ]]/plan.cache
+    reports:
+      terraform: $[[ inputs.root_dir ]]/plan.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.
+  cache:
+    key: "$[[ inputs.root_dir ]]"
+    paths:
+      - $[[ inputs.root_dir ]]/.terraform/
+  variables:
+    TF_ROOT: $[[ inputs.root_dir ]]
+    TF_STATE_NAME: $[[ inputs.state_name ]]
+  image:
+    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+  script:
+    - gitlab-tofu plan
+    - gitlab-tofu plan-json
diff --git a/templates/validate.yml b/templates/validate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a37813bf00c2877e3180a10451c0bfdabcbd2b95
--- /dev/null
+++ b/templates/validate.yml
@@ -0,0 +1,64 @@
+spec:
+  inputs:
+    # Job and Stage name
+    as:
+      default: 'validate'
+      description: 'Defines the name of this job.'
+    stage:
+      default: 'validate'
+      description: 'Defines the stage that this job will belong to.'
+
+    # Versions
+    # This version is only required, because we cannot access the context of the component,
+    # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    version:
+      default: 'latest'
+      description: 'Version of this component. Has to be the same as the one in the component include entry.'
+      
+    opentofu_version:
+      default: '1.6.0'
+      options:
+        - '$OPENTOFU_VERSION'
+        - '1.6.0'
+        - '1.6.0-rc1'
+      description: 'OpenTofu version that should be used.'
+
+    # Images
+    image_registry_base:
+      default: '$CI_REGISTRY/components/opentofu'
+    # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722
+    # gitlab_opentofu_image:
+    #   # FIXME: This should reference the component tag that is used.
+    #   #        Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275
+    #   # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]'
+    #   default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+    #   description: 'Tag of the gitlab-opentofu image.'
+    
+    # Configuration
+    root_dir: 
+      default: ${CI_PROJECT_DIR}
+      description: 'Root directory for the OpenTofu project.'
+    state_name:
+      default: default
+      description: 'Remote OpenTofu state name.'
+
+---
+
+'$[[ 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.
+  cache:
+    key: "$[[ inputs.root_dir ]]"
+    paths:
+      - $[[ inputs.root_dir ]]/.terraform/
+  variables:
+    TF_ROOT: $[[ inputs.root_dir ]]
+    TF_STATE_NAME: $[[ inputs.state_name ]]
+  image:
+    name: '$[[ inputs.image_registry_base ]]/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]'
+  script:
+    - gitlab-tofu validate