diff --git a/config/schema/search_api.processor.schema.yml b/config/schema/search_api.processor.schema.yml
index f3d37b87..9e056abf 100644
--- a/config/schema/search_api.processor.schema.yml
+++ b/config/schema/search_api.processor.schema.yml
@@ -269,3 +269,11 @@ search_api.property_configuration.search_api_url:
     absolute:
       type: boolean
       label: 'Whether to generate an absolute URL'
+
+search_api.property_configuration.custom_value:
+  type: mapping
+  label: Custom value
+  mapping:
+    value:
+      type: string
+      label: 'The field value'
diff --git a/src/Plugin/search_api/processor/CustomValue.php b/src/Plugin/search_api/processor/CustomValue.php
new file mode 100644
index 00000000..218b878d
--- /dev/null
+++ b/src/Plugin/search_api/processor/CustomValue.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\search_api\Plugin\search_api\processor;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Plugin\search_api\processor\Property\CustomValueProperty;
+use Drupal\search_api\Processor\ProcessorPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Allows adding custom tokenized text values to the index.
+ *
+ * @SearchApiProcessor(
+ *   id = "custom_value",
+ *   label = @Translation("Custom value"),
+ *   description = @Translation("Allows adding custom tokenized text values to the index."),
+ *   stages = {
+ *     "add_properties" = 0,
+ *   },
+ *   locked = true,
+ *   hidden = true,
+ * )
+ */
+class CustomValue extends ProcessorPluginBase {
+
+  /**
+   * The token service.
+   *
+   * @var \Drupal\Core\Utility\Token|null
+   */
+  protected $token;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
+    $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+    $processor->setToken($container->get('token'));
+    return $processor;
+  }
+
+  /**
+   * Retrieves the token service.
+   *
+   * @return \Drupal\Core\Utility\Token
+   *   The token service.
+   */
+  public function getToken(): Token {
+    return $this->token ?: \Drupal::token();
+  }
+
+  /**
+   * Sets the token service.
+   *
+   * @param \Drupal\Core\Utility\Token $token
+   *   The new token service.
+   *
+   * @return $this
+   */
+  public function setToken(Token $token): self {
+    $this->token = $token;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = [];
+
+    if (!$datasource) {
+      $definition = [
+        'label' => $this->t('Custom value'),
+        'description' => $this->t('Index a custom value with replacement tokens.'),
+        'type' => 'string',
+        'processor_id' => $this->getPluginId(),
+      ];
+      $properties['custom_value'] = new CustomValueProperty($definition);
+    }
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addFieldValues(ItemInterface $item) {
+    // Get all of the "custom_value" fields on this item.
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($item->getFields(), NULL, 'custom_value');
+    // If the indexed item is an entity, we can pass that as data to the token
+    // service. Otherwise, only global tokens are available.
+    $entity = $item->getOriginalObject()->getValue();
+    if ($entity instanceof EntityInterface) {
+      $data = [$entity->getEntityTypeId() => $entity];
+    }
+    else {
+      $data = [];
+    }
+
+    $token = $this->getToken();
+    foreach ($fields as $field) {
+      $config = $field->getConfiguration();
+      if (empty($config['value'])) {
+        continue;
+      }
+      // Check if there are any tokens to replace.
+      if (preg_match_all('/\[[-\w]++(?::[-\w]++)++]/', $config['value'], $matches)) {
+        $field_value = $token->replacePlain($config['value'], $data);
+        // Make sure there are no left-over tokens.
+        $field_value = str_replace($matches[0], '', $field_value);
+        $field_value = trim($field_value);
+      }
+      if ($field_value !== '') {
+        $field->addValue($field_value);
+      }
+    }
+  }
+
+}
diff --git a/src/Plugin/search_api/processor/Property/CustomValueProperty.php b/src/Plugin/search_api/processor/Property/CustomValueProperty.php
new file mode 100644
index 00000000..c24ad6bb
--- /dev/null
+++ b/src/Plugin/search_api/processor/Property/CustomValueProperty.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\search_api\Plugin\search_api\processor\Property;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\search_api\Item\FieldInterface;
+use Drupal\search_api\Processor\ConfigurablePropertyBase;
+
+/**
+ * Defines a "custom value" property.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\processor\CustomValue
+ */
+class CustomValueProperty extends ConfigurablePropertyBase {
+
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'value' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(FieldInterface $field, array $form, FormStateInterface $form_state) {
+    $configuration = $field->getConfiguration();
+
+    $form['value'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Field value'),
+      '#description' => $this->t('Use this field to set the data to be sent to the index. You can use replacement tokens depending on the type of item being indexed.'),
+      '#default_value' => $configuration['value'] ?? '',
+    ];
+
+    return $form;
+  }
+
+}
diff --git a/tests/src/Functional/ProcessorIntegrationTest.php b/tests/src/Functional/ProcessorIntegrationTest.php
index 374edaa8..807adcb2 100644
--- a/tests/src/Functional/ProcessorIntegrationTest.php
+++ b/tests/src/Functional/ProcessorIntegrationTest.php
@@ -103,6 +103,7 @@ public function testProcessorIntegration() {
       'entity_type',
       'language_with_fallback',
       'rendered_item',
+      'token_field',
     ];
     $actual_processors = array_keys($this->loadIndex()->getProcessors());
     sort($actual_processors);
@@ -230,6 +231,10 @@ public function testProcessorIntegration() {
     // locked.
     $this->checkUrlFieldIntegration();
 
+    // The 'token_field' processor is not available to be removed because it's
+    // locked.
+    $this->checkTokenFieldIntegration();
+
     // Check the order of the displayed processors.
     $stages = [
       ProcessorInterface::STAGE_PREPROCESS_INDEX,
@@ -745,6 +750,17 @@ public function checkUrlFieldIntegration() {
     $this->assertTrue($this->loadIndex()->isValidProcessor('add_url'), 'The "Add URL" processor cannot be disabled.');
   }
 
+  /**
+   * Tests the integration of the "Token field" processor.
+   */
+  public function checkTokenFieldIntegration() {
+    $index = $this->loadIndex();
+    $index->removeProcessor('token_field');
+    $index->save();
+
+    $this->assertTrue($this->loadIndex()->isValidProcessor('token_field'), 'The "Token field" processor cannot be disabled.');
+  }
+
   /**
    * Tests that a processor can be enabled.
    *
diff --git a/tests/src/Kernel/Processor/CustomValueTest.php b/tests/src/Kernel/Processor/CustomValueTest.php
new file mode 100644
index 00000000..1cf98237
--- /dev/null
+++ b/tests/src/Kernel/Processor/CustomValueTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\Tests\search_api\Kernel\Processor;
+
+use Drupal\comment\Entity\Comment;
+use Drupal\node\Entity\Node;
+use Drupal\search_api\Item\Field;
+use Drupal\search_api\Utility\Utility;
+
+/**
+ * Tests the "Custom value" processor.
+ *
+ * @group search_api
+ *
+ * @coversDefaultClass \Drupal\search_api\Plugin\search_api\processor\CustomValue
+ */
+class CustomValueTest extends ProcessorTestBase {
+
+  /**
+   * The nodes created for testing.
+   *
+   * @var \Drupal\node\Entity\Node[]
+   */
+  protected $entities = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp($processor = NULL): void {
+    parent::setUp('custom_value');
+
+    // Add three fields: one for nodes, one for comments and one for both.
+    $field = new Field($this->index, 'custom_value_nodes');
+    $field->setType('string');
+    $field->setPropertyPath('custom_value');
+    $field->setLabel('Item Type');
+    $field->setConfiguration(['value' => '[node:type]']);
+    $this->index->addField($field);
+
+    $field = new Field($this->index, 'custom_value_comments');
+    $field->setType('string');
+    $field->setPropertyPath('custom_value');
+    $field->setLabel('Comment Author');
+    $field->setConfiguration(['value' => '[comment:author]']);
+    $this->index->addField($field);
+
+    $field = new Field($this->index, 'custom_value_both');
+    $field->setType('string');
+    $field->setPropertyPath('custom_value');
+    $field->setLabel('Type/Author');
+    $field->setConfiguration(['value' => '[node:type] [comment:author]']);
+    $this->index->addField($field);
+
+    $this->index->save();
+
+    // Create a test node and test comment.
+    $this->entities['node'] = Node::create([
+      'title' => 'Test',
+      'type' => 'article',
+    ]);
+    $this->entities['node']->save();
+
+    $this->entities['comment'] = Comment::create([
+      'subject' => 'My comment title',
+      'uid' => 0,
+      'name' => 'test author',
+      'mail' => 'mail@example.com',
+      'entity_type' => 'node',
+      'field_name' => 'comment',
+      'entity_id' => $this->entities['node']->id(),
+      'comment_type' => 'node',
+      'status' => 1,
+    ]);
+    $this->entities['comment']->save();
+  }
+
+  /**
+   * Tests extracting the field for a search item.
+   *
+   * @covers ::addFieldValues
+   */
+  public function testItemFieldExtraction() {
+    // Test field value on node.
+    $node = $this->entities['node'];
+    $id = Utility::createCombinedId('entity:node', $node->id() . ':en');
+    $item = \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createItemFromObject($this->index, $node->getTypedData(), $id);
+
+    // Extract field values and check the value of our field.
+    $fields = $item->getFields();
+    $expected = ['article'];
+    $this->assertEquals($expected, $fields['custom_value_nodes']->getValues());
+    $this->assertEquals([], $fields['custom_value_comments']->getValues());
+    $this->assertEquals($expected, $fields['custom_value_both']->getValues());
+
+    // Test field value on comment.
+    $comment = $this->entities['comment'];
+    $id = Utility::createCombinedId('entity:node', $comment->id() . ':en');
+    $item = \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createItemFromObject($this->index, $comment->getTypedData(), $id);
+
+    // Extract field values and check the value of our field.
+    $fields = $item->getFields();
+    $expected = ['test author'];
+    $this->assertEquals([], $fields['custom_value_nodes']->getValues());
+    $this->assertEquals($expected, $fields['custom_value_comments']->getValues());
+    $this->assertEquals($expected, $fields['custom_value_both']->getValues());
+  }
+
+}
