diff --git a/CHANGELOG.md b/CHANGELOG.md
index f431c88d7fa01ca8fbd98a97311c16a13e9c39f5..0d9b26897f2726d55e6d021bf37b24bfb65dca2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## 1.2.0 - 2024-02-16
 
+#3405955 - Add job for CSPELL in contrib pipeline.\
 #3405813 - Create downloadable patch for ESLINT automated fixes.\
 #3419069 - Tidy up template and remove redundant lines.\
 #3421674 - Add condition for mariadb.\
diff --git a/README.MD b/README.MD
index ce0a25883590ec1c318105f11b7f91b863dc489c..0713b38dd358bc47a71fae5cca5dba1580ac6920 100644
--- a/README.MD
+++ b/README.MD
@@ -52,16 +52,17 @@ Each job will run or not run according to the following rules:
 
 | JOB NAME                    | Conditions when it will run                             | Conditions when it will NOT run                           |
 | --------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
-| composer                    | SKIP_COMPOSER_LINT=0 \|\| SKIP_PHPCS=0 \|\|<br>SKIP_STYLELINT=0 \|\| SKIP_ESLINT=0 \|\|<br>OPT_IN_TEST_CURRENT=1 | SKIP_COMPOSER_LINT=1 && SKIP_PHPCS=1 &&<br>SKIP_STYLELINT=1 && SKIP_ESLINT=1 &&<br>OPT_IN_TEST_CURRENT=0 |
+| composer                    | SKIP_COMPOSER_LINT=0 \|\| SKIP_PHPCS=0 \|\|<br>SKIP_STYLELINT=0 \|\| SKIP_ESLINT=0 \|\|<br>SKIP_CSPELL=0 \|\| OPT_IN_TEST_CURRENT=1 | SKIP_COMPOSER_LINT=1 && SKIP_PHPCS=1 &&<br>SKIP_STYLELINT=1 && SKIP_ESLINT=1 &&<br>SKIP_CSPELL=1 && OPT_IN_TEST_CURRENT=0 |
 | composer (max PHP version)  | OPT_IN_TEST_MAX_PHP=1                                   | OPT_IN_TEST_MAX_PHP=0                                     |
 | composer (previous minor)   | OPT_IN_TEST_PREVIOUS_MINOR=1                            | OPT_IN_TEST_PREVIOUS_MINOR=0                              |
 | composer (previous major)   | OPT_IN_TEST_PREVIOUS_MAJOR=1                            | OPT_IN_TEST_PREVIOUS_MAJOR=0                              |
 | composer (next minor)       | OPT_IN_TEST_NEXT_MINOR=1                                | OPT_IN_TEST_NEXT_MINOR=0                                  |
 | composer (next major)       | OPT_IN_TEST_NEXT_MAJOR=1                                | OPT_IN_TEST_NEXT_MAJOR=0                                  |
 | composer-lint               | SKIP_COMPOSER_LINT=0                                    | SKIP_COMPOSER_LINT=1                                      |
+| cspell                      | SKIP_CSPELL=0                                           | SKIP_CSPELL=1                                             |
+| eslint                      | SKIP_ESLINT=0                                           | SKIP_ESLINT=1                                             |
 | phpcs                       | SKIP_PHPCS=0                                            | SKIP_PHPCS=1                                              |
 | stylelint                   | SKIP_STYLELINT=0                                        | SKIP_STYLELINT=1                                          |
-| eslint                      | SKIP_ESLINT=0                                           | SKIP_ESLINT=1                                             |
 | phpstan                     | SKIP_PHPSTAN=0 && OPT_IN_TEST_CURRENT=1                 | SKIP_PHPSTAN=1 \|\| OPT_IN_TEST_CURRENT=0                 |
 | phpstan (next minor)        | SKIP_PHPSTAN=0 && OPT_IN_TEST_NEXT_MINOR=1              | SKIP_PHPSTAN=1 \|\| OPT_IN_TEST_NEXT_MINOR=0              |
 | phpstan (next major)        | SKIP_PHPSTAN=0 && OPT_IN_TEST_NEXT_MAJOR=1              | SKIP_PHPSTAN=1 \|\| OPT_IN_TEST_NEXT_MAJOR=0              |
diff --git a/includes/include.drupalci.main.yml b/includes/include.drupalci.main.yml
index c065a4a58be54acba4c29e06d96c67f935976e18..c0a93e2ff79389775bddd5ba574b2559771b16a1 100644
--- a/includes/include.drupalci.main.yml
+++ b/includes/include.drupalci.main.yml
@@ -85,7 +85,7 @@
 # If none of these are set then it is not needed. This rule is only used in the 'base' Composer job,
 # it is not added to any of the Composer variants because they each have their specific 'opt-in' rule.
 .check-current-composer-not-needed-rule: &check-current-composer-not-needed-rule
-  if: '$SKIP_COMPOSER_LINT == "1" && $SKIP_PHPCS == "1" && $SKIP_STYLELINT == "1" && $SKIP_ESLINT == "1" && $OPT_IN_TEST_CURRENT == "0"'
+  if: '$SKIP_COMPOSER_LINT == "1" && $SKIP_PHPCS == "1" && $SKIP_STYLELINT == "1" && $SKIP_ESLINT == "1" && $SKIP_CSPELL == "1" && $OPT_IN_TEST_CURRENT == "0"'
   when: never
 
 .skip-phpstan-rule: &skip-phpstan-rule
@@ -120,6 +120,10 @@
   if: '$SKIP_NIGHTWATCH == "1"'
   when: never
 
+.skip-cspell-rule: &skip-cspell-rule
+  if: '$SKIP_CSPELL == "1"'
+  when: never
+
 .opt-in-current-rule: &opt-in-current-rule
   if: '$OPT_IN_TEST_CURRENT != "1"'
   when: never
@@ -538,6 +542,37 @@ eslint:
     reports:
       junit: junit.xml
 
+cspell:
+  stage: validate
+  rules:
+    - *skip-cspell-rule
+    - when: on_success
+  allow_failure: true
+  needs:
+    - composer
+  artifacts:
+    expose_as: junit
+    expire_in: 6 mos
+    when: always
+    name: artifacts-$CI_PIPELINE_ID-$CI_JOB_NAME_SLUG
+    paths:
+      - _cspell_unrecognized_words_$CI_PIPELINE_ID.txt
+  script:
+    - cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME
+    - test -f .cspell.json || printf '{"words":[%s]}\n' "$_CSPELL_WORDS" > .cspell.json
+    - $CI_PROJECT_DIR/$_WEB_ROOT/core/node_modules/.bin/cspell -c .cspell.json --show-suggestions --show-context --no-progress ** || EXIT_CODE=$?
+    - WORDS_FILE=_cspell_unrecognized_words_$CI_PIPELINE_ID.txt
+    - touch $CI_PROJECT_DIR/$WORDS_FILE
+    - |
+      if [ "$EXIT_CODE" != "" ]; then
+        # There are some unrecognized words so create an artifact file containing the unique list.
+        echo "# CSPELL Unrecognized or mis-spelled words.\n# You may want to add some of these to your custom project dictionary" >> $CI_PROJECT_DIR/$WORDS_FILE
+        $CI_PROJECT_DIR/$_WEB_ROOT/core/node_modules/.bin/cspell -c .cspell.json --words-only --unique --no-progress ** | sort --ignore-case >> $CI_PROJECT_DIR/$WORDS_FILE || true
+        echo "An artifact file has been created containing a unique list of the unrecognized words, for you to browse or download."
+      fi
+    - echo "Exiting with EXIT_CODE=$EXIT_CODE"
+    - exit $EXIT_CODE
+
 ################
 # Test Jobs
 ################
diff --git a/includes/include.drupalci.variables.yml b/includes/include.drupalci.variables.yml
index 4d41fae4f9a61562b62f3cb720a0005df42249ed..e8714fc3804ede0a9f81890bb65216291f3b1efc 100644
--- a/includes/include.drupalci.variables.yml
+++ b/includes/include.drupalci.variables.yml
@@ -72,6 +72,10 @@ variables:
       value: ""
       description: "Additional options that are appended to the `stylelint` job call."
 
+    _CSPELL_WORDS:
+      value: ""
+      description: "Words to add to the cspell dictionary if the project does not have a '.cspell.json' file. Use double quotes and comma separation, for example \"drupal\", \"keycdn\"."
+
     _SHOW_ENVIRONMENT_VARIABLES:
         value: "0"
         description: "Set to 1 to show all the environment variables in the Composer and PhpUnit jobs. Known variables with Personal Identifiable Information will still be hidden. The default is 0 for no output at all."
@@ -115,6 +119,10 @@ variables:
         value: "0"
         description: "Set to 1 to skip the ESLINT CI job"
 
+    SKIP_CSPELL:
+        value: "0"
+        description: "Set to 1 to skip the cspell CI job"
+
     SKIP_PHPUNIT:
         value: "0"
         description: "Set to 1 to skip the PHPUNIT CI job"