diff --git a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php
index db0cde459f..bcdea8affd 100644
--- a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php
+++ b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php
@@ -34,7 +34,7 @@ public function __construct(ModerationInformationInterface $moderation_informati
   }
 
   /**
-   * Checks that there is a forward revision available.
+   * Checks that there is a draft revision available.
    *
    * This checker assumes the presence of an '_entity_access' requirement key
    * in the same form as used by EntityAccessCheck.
diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php
index 8f6ac4d79e..6d6081508b 100644
--- a/core/modules/content_moderation/src/EntityTypeInfo.php
+++ b/core/modules/content_moderation/src/EntityTypeInfo.php
@@ -295,7 +295,7 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id
   }
 
   /**
-   * Redirect content entity edit forms on save, if there is a forward revision.
+   * Redirect content entity edit forms on save, if there is a draft revision.
    *
    * When saving their changes, editors should see those changes displayed on
    * the next page.
diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php
index ea2fedd010..5afa9546b0 100644
--- a/core/modules/content_moderation/src/Form/EntityModerationForm.php
+++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php
@@ -140,7 +140,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $new_state = $this->moderationInfo->getWorkflowForEntity($entity)->getState($new_state);
     // The page we're on likely won't be visible if we just set the entity to
     // the default state, as we hide that latest-revision tab if there is no
-    // forward revision. Redirect to the canonical URL instead, since that will
+    // draft revision. Redirect to the canonical URL instead, since that will
     // still exist.
     if ($new_state->isDefaultRevisionState()) {
       $form_state->setRedirectUrl($entity->toUrl('canonical'));
diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php
index 862987f8d8..f4b32ab328 100644
--- a/core/modules/content_moderation/src/ModerationInformationInterface.php
+++ b/core/modules/content_moderation/src/ModerationInformationInterface.php
@@ -102,13 +102,13 @@ public function getDefaultRevisionId($entity_type_id, $entity_id);
   public function isLatestRevision(ContentEntityInterface $entity);
 
   /**
-   * Determines if a forward revision exists for the specified entity.
+   * Determines if a draft revision exists for the specified entity.
    *
    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
-   *   The entity which may or may not have a forward revision.
+   *   The entity which may or may not have a draft revision.
    *
    * @return bool
-   *   TRUE if this entity has forward revisions available, FALSE otherwise.
+   *   TRUE if this entity has draft revisions available, FALSE otherwise.
    */
   public function hasForwardRevision(ContentEntityInterface $entity);
 
diff --git a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
index eac1913fca..663516ae6f 100644
--- a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
+++ b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
@@ -48,13 +48,13 @@ public function applies($definition, $name, Route $route) {
    * Determines if the route definition includes a forward-revision flag.
    *
    * This is a custom flag defined by the Content Moderation module to load
-   * forward revisions rather than the default revision on a given route.
+   * draft revisions rather than the default revision on a given route.
    *
    * @param array $definition
    *   The parameter definition provided in the route options.
    *
    * @return bool
-   *   TRUE if the forward revision flag is set, FALSE otherwise.
+   *   TRUE if the draft revision flag is set, FALSE otherwise.
    */
   protected function hasForwardRevisionFlag(array $definition) {
     return (isset($definition['load_forward_revision']) && $definition['load_forward_revision']);
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
index 3882c764f1..e46f08312b 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php
@@ -24,8 +24,8 @@ protected function setUp() {
   /**
    * Tests the moderation form that shows on the latest version page.
    *
-   * The latest version page only shows if there is a forward revision. There
-   * is only a forward revision if a draft revision is created on a node where
+   * The latest version page only shows if there is a draft revision. There
+   * is only a draft revision if a draft revision is created on a node where
    * the default revision is not a published moderation state.
    *
    * @see \Drupal\content_moderation\EntityOperations
@@ -68,7 +68,7 @@ public function testModerationForm() {
     $this->assertField('edit-new-state', 'The node view page has a moderation form.');
 
     // The latest version page should not show, because there is still no
-    // forward revision.
+    // draft revision.
     $this->drupalGet($latest_version_path);
     $this->assertResponse(403);
 
@@ -84,11 +84,11 @@ public function testModerationForm() {
     $this->assertNoField('edit-new-state', 'The node view page has no moderation form.');
 
     // The latest version page should not show, because there is still no
-    // forward revision.
+    // draft revision.
     $this->drupalGet($latest_version_path);
     $this->assertResponse(403);
 
-    // Make a forward revision.
+    // Make a draft revision.
     $this->drupalPostForm($edit_path, [
       'body[0][value]' => 'Fourth version of the content.',
     ], t('Save and Create New Draft'));
@@ -100,7 +100,7 @@ public function testModerationForm() {
     $this->assertNoField('edit-new-state', 'The node view page has no moderation form.');
 
     // The latest version page should show the moderation form and have "Draft"
-    // status, because the forward revision is in "Draft".
+    // status, because the draft revision is in "Draft".
     $this->drupalGet($latest_version_path);
     $this->assertResponse(200);
     $this->assertField('edit-new-state', 'The latest-version page has a moderation form.');
@@ -112,7 +112,7 @@ public function testModerationForm() {
     ], t('Apply'));
 
     // The latest version page should not show, because there is no
-    // forward revision.
+    // draft revision.
     $this->drupalGet($latest_version_path);
     $this->assertResponse(403);
   }
@@ -138,7 +138,7 @@ public function testNonBundleModerationForm() {
     $this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
 
     // The latest version page should not show, because there is still no
-    // forward revision.
+    // draft revision.
     $this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
     $this->assertResponse(403);
 
@@ -152,11 +152,11 @@ public function testNonBundleModerationForm() {
     $this->assertNoText('Status', 'The node view page has no moderation form.');
 
     // The latest version page should not show, because there is still no
-    // forward revision.
+    // draft revision.
     $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
     $this->assertResponse(403);
 
-    // Make a forward revision.
+    // Make a draft revision.
     $this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
 
     // The published view should not have a moderation form, because it is the
@@ -166,7 +166,7 @@ public function testNonBundleModerationForm() {
     $this->assertNoText('Status', 'The node view page has no moderation form.');
 
     // The latest version page should show the moderation form and have "Draft"
-    // status, because the forward revision is in "Draft".
+    // status, because the draft revision is in "Draft".
     $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
     $this->assertResponse(200);
     $this->assertText('Status', 'Form text found on the latest-version page.');
@@ -178,7 +178,7 @@ public function testNonBundleModerationForm() {
     ], t('Apply'));
 
     // The latest version page should not show, because there is no
-    // forward revision.
+    // draft revision.
     $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
     $this->assertResponse(403);
   }
@@ -189,7 +189,7 @@ public function testNonBundleModerationForm() {
   public function testModerationFormSetsRevisionAuthor() {
     // Create new moderated content in published.
     $node = $this->createNode(['type' => 'moderated_content', 'moderation_state' => 'published']);
-    // Make a forward revision.
+    // Make a draft revision.
     $node->title = $this->randomMachineName();
     $node->moderation_state->value = 'draft';
     $node->save();
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php
index a9d95f0b9f..9b737316af 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php
@@ -196,7 +196,7 @@ public function testTranslateModeratedContent() {
     $this->assertTrue($french_node->isPublished());
     $this->assertFalse($english_node->isPublished());
 
-    // Create a forward revision
+    // Create a draft revision
     $this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Create New Draft (this translation)'));
     $english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
     $french_node = $english_node->getTranslation('fr');
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateBlockTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateBlockTest.php
index 1a8f78c504..81e47c2dce 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateBlockTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateBlockTest.php
@@ -93,18 +93,18 @@ public function testCustomBlockModeration() {
     $this->drupalGet('');
     $this->assertText($updated_body);
 
-    // Publish the block so we can create a forward revision.
+    // Publish the block so we can create a draft revision.
     $this->drupalPostForm('block/' . $block->id(), [], t('Save and Publish'));
 
-    // Create a forward revision.
-    $forward_revision_body = 'This is the forward revision body value';
+    // Create a draft revision.
+    $forward_revision_body = 'This is the draft revision body value';
     $edit = [
       'body[0][value]' => $forward_revision_body,
     ];
     $this->drupalPostForm('block/' . $block->id(), $edit, t('Save and Create New Draft'));
     $this->assertText(t('basic Moderated block has been updated.'));
 
-    // Navigate to home page and check that the forward revision doesn't show,
+    // Navigate to home page and check that the draft revision doesn't show,
     // since it should not be set as the default revision.
     $this->drupalGet('');
     $this->assertText($updated_body);
@@ -116,7 +116,7 @@ public function testCustomBlockModeration() {
     $this->drupalPostForm('block/' . $block->id() . '/latest', $edit, t('Apply'));
     $this->assertText(t('The moderation state has been updated.'));
 
-    // Navigate to home page and check that the forward revision is now the
+    // Navigate to home page and check that the draft revision is now the
     // default revision and therefore visible.
     $this->drupalGet('');
     $this->assertText($forward_revision_body);
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
index 277acdfadd..7d88458a5e 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
@@ -105,7 +105,7 @@ public function testFormSaveDestination() {
     $this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
     $this->assertText('Third version of the content.');
 
-    // Make a new forward revision; after saving, we should be on the "Latest
+    // Make a new draft revision; after saving, we should be on the "Latest
     // version" tab.
     $this->drupalPostForm($edit_path, [
       'body[0][value]' => 'Fourth version of the content.',
diff --git a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
index eeac1cea93..b6f9bced49 100644
--- a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
@@ -116,7 +116,7 @@ public function testPageAccess() {
     $this->drupalGet($view_path);
     $this->assertResponse(200);
 
-    // Create a forward revision for the 'Latest revision' tab.
+    // Create a draft revision for the 'Latest revision' tab.
     $this->drupalLogin($this->adminUser);
     $this->drupalPostForm($edit_path, [
       'title[0][value]' => 'moderated content revised',
@@ -132,7 +132,7 @@ public function testPageAccess() {
     $this->drupalGet($view_path);
     $this->assertResponse(200);
 
-    // Now make another user, who should not be able to see forward revisions.
+    // Now make another user, who should not be able to see draft revisions.
     $user = $this->createUser([
       'use editorial transition create_new_draft',
     ]);
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
index 60e9edf648..1b02e6e67d 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
@@ -96,7 +96,7 @@ public function testForwardRevisions() {
     $page = Node::load($id);
     $this->assertEquals('B', $page->getTitle());
 
-    // Verify we can load the forward revision, even if the mechanism is kind
+    // Verify we can load the draft revision, even if the mechanism is kind
     // of gross. Note: revisionIds() is only available on NodeStorageInterface,
     // so this won't work for non-nodes. We'd need to use entity queries. This
     // is a core bug that should get fixed.
diff --git a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php
index 2d33ee8ec4..add89fa4cb 100644
--- a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php
+++ b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php
@@ -46,7 +46,7 @@ protected function setUp() {
    * @param string $entity_type
    *   The machine name of the entity to mock.
    * @param bool $has_forward
-   *   Whether this entity should have a forward revision in the system.
+   *   Whether this entity should have a draft revision in the system.
    * @param array $account_permissions
    *   An array of permissions the account has.
    * @param bool $is_owner
@@ -119,13 +119,13 @@ public function accessSituationProvider() {
       // Node with own content permissions and no latest version, but no perms
       // to view latest version.
       [Node::class, 'node', TRUE, ['view own unpublished content'], FALSE, AccessResultNeutral::class],
-      // Block with forward revision, and permissions to view any.
+      // Block with draft revision, and permissions to view any.
       [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class],
-      // Block with no forward revision.
+      // Block with no draft revision.
       [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class],
-      // Block with forward revision, but no permission to view any.
+      // Block with draft revision, but no permission to view any.
       [BlockContent::class, 'block_content', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class],
-      // Block with no forward revision.
+      // Block with no draft revision.
       [BlockContent::class, 'block_content', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class],
     ];
   }
diff --git a/core/modules/node/src/Tests/PagePreviewTest.php b/core/modules/node/src/Tests/PagePreviewTest.php
index 37b00d3b48..226aae82eb 100644
--- a/core/modules/node/src/Tests/PagePreviewTest.php
+++ b/core/modules/node/src/Tests/PagePreviewTest.php
@@ -427,14 +427,14 @@ public function testPagePreviewWithRevisions() {
     $this->assertFieldByName('revision_log[0][value]', $edit['revision_log[0][value]'], 'Revision log field displayed.');
 
     // Save the node after coming back from the preview page so we can create a
-    // forward revision for it.
+    // draft revision for it.
     $this->drupalPostForm(NULL, [], t('Save'));
     $node = $this->drupalGetNodeByTitle($edit[$title_key]);
 
-    // Check that previewing a forward revision of a node works. This can not be
+    // Check that previewing a draft revision of a node works. This can not be
     // accomplished through the UI so we have to use API calls.
     // @todo Change this test to use the UI when we will be able to create
-    // forward revisions in core.
+    // draft revisions in core.
     // @see https://www.drupal.org/node/2725533
     $node->setNewRevision(TRUE);
     $node->isDefaultRevision(FALSE);
diff --git a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
index 38db0e9dc0..3828c834aa 100644
--- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
+++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
@@ -140,23 +140,23 @@ public function testEntityRevisionParamConverter() {
     $forward_revision->setNewRevision();
     $forward_revision->isDefaultRevision(FALSE);
 
-    $forward_revision->name = 'forward revision - en';
+    $forward_revision->name = 'draft revision - en';
     $forward_revision->save();
 
     $forward_revision_translation = $forward_revision->getTranslation('de');
-    $forward_revision_translation->name = 'forward revision - de';
+    $forward_revision_translation->name = 'draft revision - de';
     $forward_revision_translation->save();
 
     // Check that the entity revision is upcasted in the correct language.
     $revision_url = 'entity_test_mulrev/' . $entity->id() . '/revision/' . $forward_revision->getRevisionId() . '/view';
 
     $this->drupalGet($revision_url);
-    $this->assertText('forward revision - en');
-    $this->assertNoText('forward revision - de');
+    $this->assertText('draft revision - en');
+    $this->assertNoText('draft revision - de');
 
     $this->drupalGet('de/' . $revision_url);
-    $this->assertText('forward revision - de');
-    $this->assertNoText('forward revision - en');
+    $this->assertText('draft revision - de');
+    $this->assertNoText('draft revision - en');
   }
 
 }
diff --git a/core/modules/taxonomy/tests/src/Kernel/ForwardRevisionTest.php b/core/modules/taxonomy/tests/src/Kernel/ForwardRevisionTest.php
index abc0846fc5..668e5589b7 100644
--- a/core/modules/taxonomy/tests/src/Kernel/ForwardRevisionTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/ForwardRevisionTest.php
@@ -11,7 +11,7 @@
 use Drupal\taxonomy\Entity\Vocabulary;
 
 /**
- * Kernel tests for taxonomy forward revisions.
+ * Kernel tests for taxonomy draft revisions.
  *
  * @group taxonomy
  */
@@ -35,7 +35,7 @@ protected function setUp() {
   }
 
   /**
-   * Tests that the taxonomy index work correctly with forward revisions.
+   * Tests that the taxonomy index work correctly with draft revisions.
    */
   public function testTaxonomyIndexWithForwardRevision() {
     \Drupal::configFactory()->getEditable('taxonomy.settings')->set('maintain_index_table', TRUE)->save();
diff --git a/core/modules/workflows/src/Entity/Workflow.php b/core/modules/workflows/src/Entity/Workflow.php
index 51bdda2b24..00876528df 100644
--- a/core/modules/workflows/src/Entity/Workflow.php
+++ b/core/modules/workflows/src/Entity/Workflow.php
@@ -154,7 +154,7 @@ public function addState($state_id, $label) {
     if (isset($this->states[$state_id])) {
       throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'");
     }
-    if (preg_match('/[^a-z0-9_]+/', $state_id)) {
+    if (preg_match('/[^a-z0-9_]+|^[0-9]+$/', $state_id)) {
       throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
     }
     $this->states[$state_id] = [
@@ -271,7 +271,7 @@ public function addTransition($transition_id, $label, array $from_state_ids, $to
     if (isset($this->transitions[$transition_id])) {
       throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow '{$this->id()}'");
     }
-    if (preg_match('/[^a-z0-9_]+/', $transition_id)) {
+    if (preg_match('/[^a-z0-9_]+|^[0-9]+$/', $transition_id)) {
       throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores");
     }
 
diff --git a/core/modules/workflows/src/Form/WorkflowStateAddForm.php b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
index 1827dd3e89..13a99ee65e 100644
--- a/core/modules/workflows/src/Form/WorkflowStateAddForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
@@ -37,9 +37,13 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $form['id'] = [
       '#type' => 'machine_name',
+      '#description' => $this->t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores, but not solely numbers.'),
       '#machine_name' => [
         'exists' => [$this, 'exists'],
+        'replace_pattern' => '[^a-z0-9_]+|^[0-9]+$',
+        'error' => $this->t('The machine-readable name must contain only lowercase letters, numbers, and underscores, but not solely numbers.')
       ],
+      '#required' => TRUE,
     ];
 
     // Add additional form fields from the workflow type plugin.
@@ -82,16 +86,22 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
     /** @var \Drupal\workflows\WorkflowInterface $entity */
     $values = $form_state->getValues();
 
-    // Replicate the validation that Workflow::addState() does internally as the
-    // form values have not been validated at this point.
-    if (!$entity->hasState($values['id']) && !preg_match('/[^a-z0-9_]+/', $values['id'])) {
-      $entity->addState($values['id'], $values['label']);
-      if (isset($values['type_settings'])) {
-        $configuration = $entity->getTypePlugin()->getConfiguration();
-        $configuration['states'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
-        $entity->set('type_settings', $configuration);
+    try {
+      // Replicate the validation that Workflow::addState() does internally as the
+      // form values have not been validated at this point.
+      if (!$entity->hasState($values['id']) && !preg_match('/[^a-z0-9_]+|^[0-9]+$/', $values['id'])) {
+        $entity->addState($values['id'], $values['label']);
+        if (isset($values['type_settings'])) {
+          $configuration = $entity->getTypePlugin()->getConfiguration();
+          $configuration['states'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()
+            ->getPluginId()];
+          $entity->set('type_settings', $configuration);
+        }
       }
     }
+    catch (\Exception $e) {
+      // Do nothing, if there is an exception we want validation to handle it.
+    }
   }
 
   /**
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
index fe3a40636d..469daa1cd3 100644
--- a/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
+++ b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
@@ -38,9 +38,13 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $form['id'] = [
       '#type' => 'machine_name',
+      '#description' => $this->t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores, but not solely numbers.'),
       '#machine_name' => [
         'exists' => [$this, 'exists'],
+        'replace_pattern' => '[^a-z0-9_]+|^[0-9]+$',
+        'error' => $this->t('The machine-readable name must contain only lowercase letters, numbers, and underscores, but not solely numbers.')
       ],
+      '#required' => TRUE,
     ];
 
     // @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that
@@ -102,13 +106,19 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
       // Only do something once form validation is complete.
       return;
     }
-    /** @var \Drupal\workflows\WorkflowInterface $entity */
-    $values = $form_state->getValues();
-    $entity->addTransition($values['id'], $values['label'], array_filter($values['from']), $values['to']);
-    if (isset($values['type_settings'])) {
-      $configuration = $entity->getTypePlugin()->getConfiguration();
-      $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
-      $entity->set('type_settings', $configuration);
+    try {
+      /** @var \Drupal\workflows\WorkflowInterface $entity */
+      $values = $form_state->getValues();
+      $entity->addTransition($values['id'], $values['label'], array_filter($values['from']), $values['to']);
+      if (isset($values['type_settings'])) {
+        $configuration = $entity->getTypePlugin()->getConfiguration();
+        $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()
+          ->getPluginId()];
+        $entity->set('type_settings', $configuration);
+      }
+    }
+    catch (\Exception $e) {
+      // Do nothing, if there is an exception we want validation to handle it.
     }
   }
 
diff --git a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
index 7db8780194..d0025af33a 100644
--- a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
+++ b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
@@ -94,7 +94,7 @@ public function testStateMachineNameValidation() {
       'id' => 'Invalid ID',
     ], 'Save');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
+    $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores, but not solely numbers.');
   }
 
   /**
@@ -129,6 +129,12 @@ public function testWorkflowCreation() {
     $workflow = $workflow_storage->loadUnchanged('test');
     $this->assertFalse($workflow->getState('draft')->canTransitionTo('draft'), 'Can not transition from draft to draft');
 
+    $this->clickLink('Add a new state');
+    // Don't allow numeric only machine names.
+    $this->submitForm(['label' => 'foo', 'id' => '123'], 'Save');
+    $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores, but not solely numbers.');
+
+    $this->drupalGet('/admin/config/workflow/workflows/manage/test');
     $this->clickLink('Add a new transition');
     $this->submitForm(['id' => 'publish', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published'], 'Save');
     $this->assertSession()->pageTextContains('Created Publish transition.');
@@ -136,6 +142,11 @@ public function testWorkflowCreation() {
     $this->assertTrue($workflow->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published');
 
     $this->clickLink('Add a new transition');
+    $this->submitForm(['id' => '123', 'label' => 'Foo', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save');
+    $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores, but not solely numbers.');
+
+    $this->drupalGet('/admin/config/workflow/workflows/manage/test');
+    $this->clickLink('Add a new transition');
     $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save');
     $this->assertSession()->pageTextContains('Created Create new draft transition.');
     $workflow = $workflow_storage->loadUnchanged('test');
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php
index 5c7e51cbc5..f98c40d7b1 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php
@@ -914,7 +914,7 @@ public function testForwardRevisions() {
       ->execute();
     $this->assertEqual(count($result), 1);
 
-    // Verify that field conditions on the default and forward revision are
+    // Verify that field conditions on the default and draft revision are
     // work as expected.
     $result = \Drupal::entityQuery('entity_test_mulrev')
       ->condition('id', [14], 'IN')
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
index 5c0ef7b382..ec121833fa 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
@@ -88,7 +88,7 @@ public function testRevertRevisionAfterTranslation() {
   }
 
   /**
-   * Tests the translation values when saving a forward revision.
+   * Tests the translation values when saving a draft revision.
    */
   public function testTranslationValuesWhenSavingForwardRevisions() {
     $user = $this->createUser();
@@ -103,18 +103,18 @@ public function testTranslationValuesWhenSavingForwardRevisions() {
     $entity->addTranslation('de', ['name' => 'default revision - de']);
     $entity->save();
 
-    // Create a forward revision for the entity and change a field value for
+    // Create a draft revision for the entity and change a field value for
     // both languages.
     $forward_revision = $this->reloadEntity($entity);
 
     $forward_revision->setNewRevision();
     $forward_revision->isDefaultRevision(FALSE);
 
-    $forward_revision->name = 'forward revision - en';
+    $forward_revision->name = 'draft revision - en';
     $forward_revision->save();
 
     $forward_revision_translation = $forward_revision->getTranslation('de');
-    $forward_revision_translation->name = 'forward revision - de';
+    $forward_revision_translation->name = 'draft revision - de';
     $forward_revision_translation->save();
 
     $forward_revision_id = $forward_revision->getRevisionId();
@@ -122,14 +122,14 @@ public function testTranslationValuesWhenSavingForwardRevisions() {
 
     // Change the value of the field in the default language, save the forward
     // revision and check that the value of the field in the second language is
-    // also taken from the forward revision, *not* from the default revision.
-    $forward_revision->name = 'updated forward revision - en';
+    // also taken from the draft revision, *not* from the default revision.
+    $forward_revision->name = 'updated draft revision - en';
     $forward_revision->save();
 
     $forward_revision = $storage->loadRevision($forward_revision_id);
 
-    $this->assertEquals($forward_revision->name->value, 'updated forward revision - en');
-    $this->assertEquals($forward_revision->getTranslation('de')->name->value, 'forward revision - de');
+    $this->assertEquals($forward_revision->name->value, 'updated draft revision - en');
+    $this->assertEquals($forward_revision->getTranslation('de')->name->value, 'draft revision - de');
   }
 
   /**
