Repository: RasaHQ/rasa
Pull request: update documentation for custom components / policies
**Proposed changes**:
- fix https://github.com/RasaHQ/rasa/issues/9885
- create one central place which documents how to write custom NLU components **and** custom policies
- link to this central place for policies / components
- add migration guide for policies / nlu components
- add changelog for architecture revamp

**Open Todos**
- [x] add changelog for the overall feature
- [x] add recipe thing to migration guide
- [x] add guide for custom NLU components
- [x] polish guide

**Status (please check what you already did)**:
- [x] added some tests for the functionality
- [x] updated the documentation
- [x] updated the changelog (please check [changelog](https://github.com/RasaHQ/rasa/tree/main/changelog) for instructions)
- [ ] reformat files using `black` (please check [Readme](https://github.com/RasaHQ/rasa#code-style) for instructions)


Problem statement:
Update documentation with architecture related changes to Policies and NLU components
**Overview**
We need to update the documentation with the user implications related to the architecture revamp.

**Definition of Done**
- [x] add migration guides for `Policy` / `NLU component`
- [ ] add docs link in relevant pieces of the code (e.g. in `rasa/engine/validation`)
- [x] rewrite guides for creating custom policies / NLU components
- [x] update code chunks in docs
- [ ] Have Vincent review this

Hints from issue discussion:

Commit message: append `.mdx` suffix everywhere

Patch to review:
diff --git a/changelog/9803.feature.md b/changelog/9803.feature.md
new file mode 100644
index 000000000000..b3df0b3716d0
--- /dev/null
+++ b/changelog/9803.feature.md
@@ -0,0 +1,16 @@
+Rasa Open Source now uses the passed in [model configuration](model-configuration.mdx) to build a
+[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
+This graph describes the dependencies between the items in your model configuration and
+how data flows between them. This has two major benefits:
+
+- Rasa Open Source can use the computational graph to optimize the execution of your
+  model. Examples for this are efficient caching of training steps or executing
+  independent steps in parallel.
+- Rasa Open Source can represent different model architectures flexibly. As long as the
+  graph remains acyclic Rasa Open Source can in theory pass any data to any graph
+  component based on the model configuration without having to tie the underlying
+  software architecture to the used model architecture.
+
+This change required changes to custom policies and custom NLU components. See the
+documentation for a detailed 
+[migration guide](migration-guide.mdx#custom-policies-and-custom-components).
diff --git a/data/test_classes/graph_component_interface.py b/data/test_classes/graph_component_interface.py
new file mode 100644
index 000000000000..f4d2d005bef3
--- /dev/null
+++ b/data/test_classes/graph_component_interface.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+from abc import ABC, abstractmethod
+from typing import List, Type, Dict, Text, Any, Optional
+
+from rasa.engine.graph import ExecutionContext
+from rasa.engine.storage.resource import Resource
+from rasa.engine.storage.storage import ModelStorage
+
+
+class GraphComponent(ABC):
+    """Interface for any component which will run in a graph."""
+
+    @classmethod
+    def required_components(cls) -> List[Type]:
+        """Components that should be included in the pipeline before this component."""
+        return []
+
+    @classmethod
+    @abstractmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> GraphComponent:
+        """Creates a new `GraphComponent`.
+
+        Args:
+            config: This config overrides the `default_config`.
+            model_storage: Storage which graph components can use to persist and load
+                themselves.
+            resource: Resource locator for this component which can be used to persist
+                and load itself from the `model_storage`.
+            execution_context: Information about the current graph run.
+
+        Returns: An instantiated `GraphComponent`.
+        """
+        ...
+
+    @classmethod
+    def load(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+        **kwargs: Any,
+    ) -> GraphComponent:
+        """Creates a component using a persisted version of itself.
+
+        If not overridden this method merely calls `create`.
+
+        Args:
+            config: The config for this graph component. This is the default config of
+                the component merged with config specified by the user.
+            model_storage: Storage which graph components can use to persist and load
+                themselves.
+            resource: Resource locator for this component which can be used to persist
+                and load itself from the `model_storage`.
+            execution_context: Information about the current graph run.
+            kwargs: Output values from previous nodes might be passed in as `kwargs`.
+
+        Returns:
+            An instantiated, loaded `GraphComponent`.
+        """
+        return cls.create(config, model_storage, resource, execution_context)
+
+    @staticmethod
+    def get_default_config() -> Dict[Text, Any]:
+        """Returns the component's default config.
+
+        Default config and user config are merged by the `GraphNode` before the
+        config is passed to the `create` and `load` method of the component.
+
+        Returns:
+            The default config of the component.
+        """
+        return {}
+
+    @staticmethod
+    def supported_languages() -> Optional[List[Text]]:
+        """Determines which languages this component can work with.
+
+        Returns: A list of supported languages, or `None` to signify all are supported.
+        """
+        return None
+
+    @staticmethod
+    def not_supported_languages() -> Optional[List[Text]]:
+        """Determines which languages this component cannot work with.
+
+        Returns: A list of not supported languages, or
+            `None` to signify all are supported.
+        """
+        return None
+
+    @staticmethod
+    def required_packages() -> List[Text]:
+        """Any extra python dependencies required for this component to run."""
+        return []
diff --git a/data/test_classes/nlu_component_skeleton.py b/data/test_classes/nlu_component_skeleton.py
new file mode 100644
index 000000000000..2f9711c72aaf
--- /dev/null
+++ b/data/test_classes/nlu_component_skeleton.py
@@ -0,0 +1,42 @@
+from typing import Dict, Text, Any, List
+
+from rasa.engine.graph import GraphComponent, ExecutionContext
+from rasa.engine.recipes.default_recipe import DefaultV1Recipe
+from rasa.engine.storage.resource import Resource
+from rasa.engine.storage.storage import ModelStorage
+from rasa.shared.nlu.training_data.message import Message
+from rasa.shared.nlu.training_data.training_data import TrainingData
+
+# TODO: Correctly register your component
+@DefaultV1Recipe.register(
+    [DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER], is_trainable=True
+)
+class CustomNLUComponent(GraphComponent):
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> GraphComponent:
+        # TODO: Implement this
+        ...
+
+    def train(self, training_data: TrainingData) -> Resource:
+        # TODO: Implement this if your component requires training
+        ...
+
+    def process_training_data(self, training_data: TrainingData) -> TrainingData:
+        # TODO: Implement this if your component augments the training data with
+        #       tokens or message features which are used by other components
+        #       during training.
+        # components during training.
+        ...
+
+        return training_data
+
+    def process(self, messages: List[Message]) -> List[Message]:
+        # TODO: This is the method which Rasa Open Source will call during inference.
+        ...
+        return messages
diff --git a/data/test_classes/registered_component.py b/data/test_classes/registered_component.py
new file mode 100644
index 000000000000..300dad412d22
--- /dev/null
+++ b/data/test_classes/registered_component.py
@@ -0,0 +1,11 @@
+from rasa.engine.graph import GraphComponent
+from rasa.engine.recipes.default_recipe import DefaultV1Recipe
+
+
+@DefaultV1Recipe.register(
+    component_types=[DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER],
+    is_trainable=True,
+    model_from="SpacyNLP",
+)
+class MyComponent(GraphComponent):
+    ...
diff --git a/docs/docs/business-logic.mdx b/docs/docs/business-logic.mdx
index 6d8d13950300..ce3b21bdf402 100644
--- a/docs/docs/business-logic.mdx
+++ b/docs/docs/business-logic.mdx
@@ -106,7 +106,7 @@ to your NLU pipeline:
 language: en
 pipeline:
 # other components
-- DucklingEntityExtractor:
+- name: DucklingEntityExtractor
   dimensions: ["number"]
 ```
 
diff --git a/docs/docs/components.mdx b/docs/docs/components.mdx
index bd77aa49e7c6..4c1924cb5d55 100644
--- a/docs/docs/components.mdx
+++ b/docs/docs/components.mdx
@@ -2770,8 +2770,16 @@ See [starspace paper](https://arxiv.org/abs/1709.03856) for details.
 
 ## Custom Components
 
+:::info New in 3.0
+Rasa Open Source 3.0 unified the implementation of NLU components and policies.
+This requires changes to custom components written for earlier versions of Rasa Open
+Source. Please see the
+[migration guide](migration-guide.mdx#custom-policies-and-custom-components) for a
+step-by-step guide for the migration.
+
+:::
+
 You can create a custom component to perform a specific task which NLU doesn't currently offer (for example, sentiment analysis).
-Below is the specification of the `rasa.nlu.components.Component`] class with the methods you'll need to implement.
 
 You can add a custom component to your pipeline by adding the module path.
 So if you have a module called `sentiment`
@@ -2782,133 +2790,6 @@ pipeline:
 - name: "sentiment.SentimentAnalyzer"
 ```
 
+See the [guide on custom components](custom-graph-components.mdx) for a full guide on this.
 Also be sure to read the section on the [Component Lifecycle](./tuning-your-model.mdx#component-lifecycle).
 
-To get started, you can use this skeleton that contains the most important
-methods that you should implement:
-
-```python
-import typing
-from typing import Any, Optional, Text, Dict, List, Type
-
-from rasa.nlu.components import Component
-from rasa.nlu.config import RasaNLUModelConfig
-from rasa.shared.nlu.training_data.training_data import TrainingData
-from rasa.shared.nlu.training_data.message import Message
-
-if typing.TYPE_CHECKING:
-    from rasa.nlu.model import Metadata
-
-
-class MyComponent(Component):
-    """A new component"""
-
-    # Which components are required by this component.
-    # Listed components should appear before the component itself in the pipeline.
-    @classmethod
-    def required_components(cls) -> List[Type[Component]]:
-        """Specify which components need to be present in the pipeline."""
-
-        return []
-
-    # Defines the default configuration parameters of a component
-    # these values can be overwritten in the pipeline configuration
-    # of the model. The component should choose sensible defaults
-    # and should be able to create reasonable results with the defaults.
-    defaults = {}
-
-    # Defines what language(s) this component can handle.
-    # This attribute is designed for instance method: `can_handle_language`.
-    # Default value is None which means it can handle all languages.
-    # This is an important feature for backwards compatibility of components.
-    supported_language_list = None
-
-    # Defines what language(s) this component can NOT handle.
-    # This attribute is designed for instance method: `can_handle_language`.
-    # Default value is None which means it can handle all languages.
-    # This is an important feature for backwards compatibility of components.
-    not_supported_language_list = None
-
-    def __init__(self, component_config: Optional[Dict[Text, Any]] = None) -> None:
-        super().__init__(component_config)
-
-    def train(
-        self,
-        training_data: TrainingData,
-        config: Optional[RasaNLUModelConfig] = None,
-        **kwargs: Any,
-    ) -> None:
-        """Train this component.
-
-        This is the components chance to train itself provided
-        with the training data. The component can rely on
-        any context attribute to be present, that gets created
-        by a call to :meth:`components.Component.pipeline_init`
-        of ANY component and
-        on any context attributes created by a call to
-        :meth:`components.Component.train`
-        of components previous to this one."""
-        pass
-
-    def process(self, message: Message, **kwargs: Any) -> None:
-        """Process an incoming message.
-
-        This is the components chance to process an incoming
-        message. The component can rely on
-        any context attribute to be present, that gets created
-        by a call to :meth:`components.Component.pipeline_init`
-        of ANY component and
-        on any context attributes created by a call to
-        :meth:`components.Component.process`
-        of components previous to this one."""
-        pass
-
-    def persist(self, file_name: Text, model_dir: Text) -> Optional[Dict[Text, Any]]:
-        """Persist this component to disk for future loading."""
-
-        pass
-
-    @classmethod
-    def load(
-        cls,
-        meta: Dict[Text, Any],
-        model_dir: Text,
-        model_metadata: Optional["Metadata"] = None,
-        cached_component: Optional["Component"] = None,
-        **kwargs: Any,
-    ) -> "Component":
-        """Load this component from file."""
-
-        if cached_component:
-            return cached_component
-        else:
-            return cls(meta)
-
-```
-
-When you [define metadata for your intent examples in your training data](./training-data-format.mdx#training-examples),
-your component can access both the intent metadata and the intent example metadata during processing:
-
-```python
-# in your component class
-
-    def process(self, message: Message, **kwargs: Any) -> None:
-        metadata = message.get("metadata")
-        print(metadata.get("intent"))
-        print(metadata.get("example"))
-```
-
-:::note custom tokenizers
-If you create a custom tokenizer you should implement the methods of `rasa.nlu.tokenizers.tokenizer.Tokenizer`.
-The `train` and `process` methods are already implemented and you simply need to overwrite the `tokenize`
-method.
-
-:::
-
-:::note custom featurizers
-If you create a custom featurizer you can return two different kind of features: sequence features and sentence
-features. The sequence features are a matrix of size `(number-of-tokens x feature-dimension)`, e.g.
-the matrix contains a feature vector for every token in the sequence.
-The sentence features are represented by a matrix of size `(1 x feature-dimension)`.
-
-:::
diff --git a/docs/docs/custom-graph-components.mdx b/docs/docs/custom-graph-components.mdx
new file mode 100644
index 000000000000..68bf730b1e9f
--- /dev/null
+++ b/docs/docs/custom-graph-components.mdx
@@ -0,0 +1,429 @@
+---
+id: custom-graph-components
+sidebar_label: Custom Graph Components
+title: Custom Graph Components
+abstract: You can extend Rasa Open Source with custom NLU components and policies. This page provides a guide on how to develop your own custom graph components.
+---
+
+Rasa Open Source provides a variety of [NLU components](components.mdx) and
+[policies](policies.mdx) out of the box. You can customize them or create your
+own components from scratch by using custom graph components.
+
+To use your custom graph component with Rasa Open Source it has to fulfill the following
+requirements:
+- It has to implement the [`GraphComponent` interface](#the-graphcomponent-interface)
+- It has to be [registered](#registering-graph-components-with-the-model-configuration)
+  with the used model configuration
+- It has to be used in the [configuration file](#using-custom-components-in-your-model-configuration)
+- It has to use type annotations. Rasa Open Source makes use of the type annotations
+  to validate your model configuration.
+  [Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references)
+  are not allowed. If you're using Python 3.7 you can use
+  [`from __future__ import annotations`](https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7)
+  to get rid of forward references.
+
+
+## Graph Components
+
+Rasa Open Source uses the passed in [model configuration](model-configuration.mdx) to build a
+[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
+This graph describes the dependencies between the items in your model configuration and
+how data flows between them. This has two major benefits:
+
+- Rasa Open Source can use the computational graph to optimize the execution of your
+  model. Examples for this are efficient caching of training steps or executing
+  independent steps in parallel.
+- Rasa Open Source can represent different model architectures flexibly. As long as the
+  graph remains acyclic Rasa Open Source can in theory pass any data to any graph
+  component based on the model configuration without having to tie the underlying
+  software architecture to the used model architecture.
+
+When translating the model configuration to the computational graph
+[policies](policies.mdx) and [NLU components](components.mdx) become nodes within this graph.
+While there is a distinction between policies and NLU components in your model
+configuration the distinction is abstracted away when they are placed within the graph.
+At this point policies and NLU components become abstract *graph components*.
+In practice this is represented by the
+[`GraphComponent`](custom-graph-components.mdx#the-graphcomponent-interface)
+interface: Both policies and NLU components have to inherit from this interface to
+become compatible and executable for Rasa's graph.
+
+## Getting Started
+Before you get started you have to decide whether you want to implement a custom
+[NLU component](components.mdx) or a [policy](policies.mdx). If you are implementing
+a custom policy then we recommend extending the existing
+`rasa.core.policies.policy.Policy` class.
+
+```python
+from rasa.core.policies.policy import Policy
+
+# TODO: Correctly register your graph component
+@DefaultV1Recipe.register(
+    [DefaultV1Recipe.ComponentType.POLICY_WITHOUT_END_TO_END_SUPPORT], is_trainable=True
+)
+class MyPolicy(Policy):
+    ...
+```
+
+If you want to implement a custom NLU component then start out with the following
+skeleton:
+
+```python (docs/sources/data/test_classes/nlu_component_skeleton.py)
+```
+:::note custom tokenizers
+If you create a custom tokenizer you should implement the methods of
+`rasa.nlu.tokenizers.tokenizer.Tokenizer`. The `train` and `process` methods are
+already implemented so you only need to overwrite the `tokenize` method.
+
+:::
+
+
+
+## The `GraphComponent` interface
+To run your custom NLU component or policy with Rasa Open Source it has
+to implement the `GraphComponent` interface.
+
+```python (docs/sources/data/test_classes/graph_component_interface.py)
+```
+
+### `create`
+The `create` method is used to instantiate your graph component during training and has to be
+overridden. Rasa Open Source passes the following parameters when calling the method:
+
+- `config`: This is your model's default configuration merged with the
+   configuration provided to the graph component in the model configuration file.
+- `model_storage`: You can use this to persist and load your graph components. See
+   [model persistence](#model-persistence) section for further details on its usage.
+- `resource`: This uniquely identifies your component within the `model_storage`.
+   See the [model persistence](#model-persistence) section for further
+   details on its usage.
+- `execution_context`: This provides additional information about the current
+   mode of execution:
+   - `model_id`: A unique identifier for the model used during inference. This
+      parameter is `None` during training.
+   - `should_add_diagnostic_data`: If `True` then additional diagnostic metadata
+      can be added to your graph component's predictions on top of the actual prediction.
+   - `is_finetuning`: If `True` then the graph component is trained using
+     [finetuning](command-line-interface.mdx#incremental-training).
+   - `graph_schema`: The `graph_schema` describes the computational graph which is used
+     to train your assistant or to make predictions with it.
+   - `node_name`: The `node_name` is a unique identifier for the step in the graph
+     schema which is fulfilled by the called graph component
+
+### `load`
+The `load` method is used to instantiate your graph component during inference. The default
+implementation of this method calls your `create` method. It is recommended to override
+this if your graph component
+[persists data as part of the training](#model-persistence).
+See [`create`](#create) for a description of the single parameters.
+
+### `get_default_config`
+The method `get_default_config` returns the default configuration for your graph
+component. Its default implementation returns an empty dictionary which implies that
+the graph component
+does not have any configuration. Rasa Open Source will update the configuration given
+in the configuration file with the default configuration during runtime.
+
+### `supported_languages`
+The method `supported_languages` specifies which languages a graph component supports.
+Rasa Open Source will use the `language` key in the model configuration file to
+validate that the graph component is valid for usage with the specified language.
+If a graph component returns `None` (this is the default implementation), it indicates
+that the graph component supports all languages which are not part of
+`not_supported_languages`.
+
+   Examples:
+   - `[]`: The graph component does not support any language
+   - `None`: All languages are supported expect the languages defined in
+     `not_supported_languages`
+   - `["en"]`: The graph component can only be used with English conversations.
+
+### `not_supported_languages`
+The method `not_supported_languages` specifies which languages your
+graph component does not support. Rasa Open Source will use the `language` key in the
+model configuration file to validate that your graph component is valid for usage with
+the specified language. If your graph component returns `None` (this is the default
+implementation), you indicate that it supports all languages which are specified in
+`supported_languages`.
+
+   Examples:
+   - `None` or `[]`: All languages specified in `supported_languages` are supported.
+   - `["en"]`: The graph component can be used with any language except English.
+
+### `required_packages`
+The `required_packages` method indicates which Python packages need to
+be installed to be able to use this graph component. Rasa Open Source will raise an error
+during execution if your graoh component is used without the required libraries being
+installed. By default, this method returns an empty list which implies that your graph
+component does not have any extra dependencies.
+
+Examples:
+- `[]`: No extra packages are required to use this graph component
+- `["spacy"]`: The Python package `spacy` needs to be installed to be able to run
+  this graph component.
+
+## Model Persistence
+Some graph components require persisting data during training which should be available
+to the graph component during inference time. A typical use case for this is storing model
+weights. Rasa Open Source provides the
+`model_storage` and `resource` parameters to your graph component's `create` and `load`
+method for this purpose as shown in the snippet below. The `model_storage` provides
+access to data from all graph components. The `resource` allows you to uniquely identify
+your graph component's location in the model storage.
+
+
+```python {6,7,16,17}
+class MyComponent(GraphComponent):
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> MyComponent:
+        ...
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+        **kwargs: Any
+    ) -> MyComponent:
+        ...
+```
+
+### Writing to the Model Storage
+
+The snippet below illustrates how to write your graph component's data to the model
+storage.
+It is recommended to store `model_storage` and `resource` at initialization time and
+then use them at the end of your component's `train` method to handle the persistence
+of your graph component.
+
+The `self._model_storage.write_to(self._resource)` context manager provides a path to
+a directory where you can persist any data required by your
+graph component. Note that it is required to return your graph component's `resource`
+at the end of your graph component's `train` method so that Rasa Open Source can cache
+the training results between trainings.
+
+```python
+class MyComponent(GraphComponent):
+
+    def __init__(
+        self,
+        model_storage: ModelStorage,
+        resource: Resource,
+        training_artifact: Optional[Dict],
+    ) -> None:
+        # Store both `model_storage` and `resource` as object attributes to be able
+        # to utilize them at the end of the training
+        self._model_storage = model_storage
+        self._resource = resource
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> MyComponent:
+        return cls(model_storage, resource, training_artifact=None)
+
+    def train(self, training_data: TrainingData) -> Resource:
+        # Train your graph component
+        ...
+
+        # Persist your graph component
+        with self._model_storage.write_to(self._resource) as directory_path:
+            with open(directory_path / "artifact.json", "w") as file:
+                json.dump({"my": "training artifact"}, file)
+
+        # Return resource to make sure the training artifacts
+        # can be cached.
+        return self._resource
+
+```
+
+### Reading from the Model Storage
+
+When Rasa Open Source calls `load` to instantiate your graph component for
+inference you can use the counterpart `self._model_storage.read_from(resource)` to
+get access to the same directory. Using the provided path you can then load the
+persisted data and initialize your graph component with it. Note that the `model_storage`
+will throw a `ValueError` in case no persisted data was found for the given `resource`.
+
+```python
+class MyComponent(GraphComponent):
+
+    def __init__(
+        self,
+        model_storage: ModelStorage,
+        resource: Resource,
+        training_artifact: Optional[Dict],
+    ) -> None:
+        self._model_storage = model_storage
+        self._resource = resource
+
+    @classmethod
+    def load(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+        **kwargs: Any,
+    ) -> MyComponent:
+        try:
+            with model_storage.read_from(resource) as directory_path:
+                with open(directory_path / "artifact.json", "r") as file:
+                    training_artifact = json.load(file)
+                    return cls(
+                        model_storage, resource, training_artifact=training_artifact
+                    )
+        except ValueError:
+            # This allows you to handle the case if there was no
+            # persisted data for your component
+            ...
+```
+
+
+## Registering Graph Components with the Model Configuration
+To make your graph component available to Rasa Open Source you have to register your
+graph component
+with the model configuration. This is done with the `DefaultV1Recipe.register`
+decorator:
+
+```python (docs/sources/data/test_classes/registered_component.py) {5-9}
+```
+
+Rasa Open Source uses the information provided in the `register` decorator and the
+position of your graph component within the configuration file to schedule the execution
+of your graph component with its required data. The `DefaultV1Recipe.register` allows you
+to specify the following details:
+
+- `component_types`: This specifies what purpose your graph component fulfills within the
+   assistant. It is possible to specify multiple types (e.g. if your graph component is both
+   intent classifier and entity extractor):
+   * `ComponentType.MODEL_LOADER`: Component type for
+      [language models](components.mdx#language-models). Graph components of this type provide
+      pretrained models to other graph components' `train`, `process_training_data` and
+      `process` methods if they have specified `model_from=<model loader name>`.
+      This graph component is run during training and
+      inference. Rasa Open Source will use the graph component's `provide` method to
+      retrieve the model which should be provided to dependent graph components.
+
+   * `ComponentType.MESSAGE_TOKENIZER`: Component type for
+      [tokenizers](components.mdx#tokenizers). This graph component is run during training and
+      inference. Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      `process_training_data` for tokenizing training data examples and `process`
+      to tokenize messages during inference.
+
+   * `ComponentType.MESSAGE_FEATURIZER`: Component type for
+      [featurizers](components.mdx#featurizers). This graph component is run during training and
+      inference. Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      `process_training_data` for featurizing training data examples and `process`
+      to featurize messages during inference.
+
+   * `ComponentType.INTENT_CLASSIFIER`: Component type for
+      [intent classifiers](components.mdx#intent-classifiers). This graph component is run only
+      during training if `is_trainable=True`. The graph component is always run during
+      inference.
+      Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      the graph component's `process` method to classify the intent of messages during
+      inference.
+
+   * `ComponentType.ENTITY_EXTRACTOR`: Component type for
+      [entity extractors](components.mdx#entity-extractors). This graph component is run only
+      during training if `is_trainable=True`. The graph component is always run during
+      inference.
+      Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      the graph scomponent's `process` method to extract entities during inference.
+
+   * `ComponentType.POLICY_WITHOUT_END_TO_END_SUPPORT`: Component type for
+      [policies](policies.mdx) which don't require additional end-to-end features
+      (see [end-to-end training](stories.mdx#end-to-end-training) for more information).
+      This graph component is run only during training if `is_trainable=True`.
+      The graph component is always run during inference.
+      Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      the graph component's `predict_action_probabilities` to make predictions for the
+      next action which should be run within a conversation.
+
+   * `ComponentType.POLICY_WITH_END_TO_END_SUPPORT`: Component type for
+      [policies](policies.mdx) which require additional end-to-end features
+      (see [end-to-end training](stories.mdx#end-to-end-training) for more information).
+      The end-to-end features are passed into the graph component's `train` and
+      `predict_action_probabilities` as `precomputations` parameter.
+      This graph component is run only during training if `is_trainable=True`.
+      The graph component is always run during inference.
+      Rasa Open Source will use the graph component's `train` method if
+      `is_trainable=True` is specified. Rasa Open Source will use
+      the graph component's `predict_action_probabilities` to make predictions for the
+      next action which should be run within a conversation.
+
+- `is_trainable`: Specifies if the graph component requires to train itself before it can
+   process training data for other dependent graph components or before it can make
+   predictions
+
+- `model_from`: Specifies if a pretrained model needs to be provided to the `train`,
+  `process_training_data` and `process` methods of the graph component. Note that you
+   still need to make sure that the model provided graph component is part of your model
+   configuration.
+
+## Using Custom Components in your Model Configuration
+You can use custom components like any other NLU component or policy within your
+[model configuration](model-configuration.mdx). The only change is that you have to specify
+the full module name instead of the class name only. The full module name depends on
+your module's location in relation to the specified
+[PYTHONPATH](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH).
+By default, Rasa Open Source adds the directory from where you run the CLI to the
+`PYTHONPATH`. If you e.g. run the CLI from `/Users/<user>/my-rasa-project`
+and your module `MyComponent` is in `/Users/<user>/my-rasa-project/custom_components/my_component.py`
+then the module path is `custom_components.my_component.MyComponent`. Everything except
+the `name` entry will be passed as `config` to your component.
+
+```yaml-rasa {5-6,10} title="config.yml"
+recipe: default.v1
+language: en
+pipeline:
+# other NLU components
+- name: your.custom.NLUComponent
+  dimensions: ["number"]
+
+policies:
+# other dialogue policies
+- name: your.custom.Policy
+```
+
+## Implementation Hints
+
+### Message Metadata
+When you [define metadata for your intent examples in your training data](./training-data-format.mdx#training-examples),
+your NLU component can access both the intent metadata and the intent example metadata during processing:
+
+```python
+# in your component class
+
+    def process(self, message: Message, **kwargs: Any) -> None:
+        metadata = message.get("metadata")
+        print(metadata.get("intent"))
+        print(metadata.get("example"))
+```
+
+### Sparse and Dense Message Features
+If you create a custom message featurizer you can return two different kind of features: sequence features and sentence
+features. The sequence features are a matrix of size `(number-of-tokens x feature-dimension)`, e.g.
+the matrix contains a feature vector for every token in the sequence.
+The sentence features are represented by a matrix of size `(1 x feature-dimension)`.
+
+## Examples of Custom Components
+Vincent?
diff --git a/docs/docs/migration-guide.mdx b/docs/docs/migration-guide.mdx
index 34d43d4fa263..d9c252bcbd17 100644
--- a/docs/docs/migration-guide.mdx
+++ b/docs/docs/migration-guide.mdx
@@ -6,6 +6,8 @@ description: |
   Information about changes between major versions of chatbot framework Rasa Core
   and how you can migrate from one version to another.
 ---
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
 
 This page contains information about changes between major versions and
 how you can migrate from one version to another.
@@ -24,6 +26,904 @@ If you still have training data in Markdown format then the recommended approach
 to convert your data from Markdown to YAML. Please use the commands described
 [here](./migration-guide.mdx#training-data-files).
 
+### Model Configuration
+
+It is required to specify the used `recipe` within the
+[model configuration](model-configuration.mdx). As of now Rasa Open Source only supports
+the `default.v1` recipe and will continue using this even if you don't specify this
+in the model configuration. To avoid breaking changes in the future it is recommended
+to specify `recipe: "default.v1"` at the top of your model configuration:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```yaml-rasa
+  language: en
+
+  pipeline:
+    ...
+  policies:
+    ...
+  ```
+  </TabItem>
+  <TabItem value="new">
+
+  ```yaml-rasa {1}
+  recipe: default.v1
+
+  language: en
+
+  pipeline:
+    ...
+  policies:
+    ...
+  ```
+  </TabItem>
+</Tabs>
+
+### Custom Policies and Custom Components
+
+Rasa Open Source 3.0 changed the way how [NLU components](components.mdx) and
+[policies](policies.mdx) are trained and run during inference. As part of these changes
+the interfaces for NLU components and policies have been unified and adapted.
+
+The next sections outline which adaptions are required to run your custom NLU components
+and policies with Rasa Open Source 3.0.
+
+:::caution
+
+Please read the updated guide on custom graph components [here](custom-graph-components.mdx) before
+continuing to follow the step-by-step guide to migrate your own custom graph components.
+:::
+
+**Type Annotations**
+
+Until Rasa Open Source 3.0 [type annotations](https://docs.python.org/3/library/typing.html)
+were not required in custom policies or custom NLU components.
+It is now required to use
+[type annotations](https://docs.python.org/3/library/typing.html) in custom NLU
+components and policies. Rasa Open Source uses these type annotations to validate that
+your graph components are compatible and correctly configured. As outlined in the custom
+[components guide](custom-graph-components.mdx) it is not allowed to use
+[Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references).
+
+:::info Forward References with Python 3.7
+Use the
+[`from __future__ import annotations`](https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7)
+import to avoid using forward references with Python 3.7.
+:::
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-type-annotations" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List["TrackerWithCachedStates"],
+          domain,
+          **kwargs: Any,
+      ):
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  from __future__ import annotations
+
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          **kwargs: Any,
+      ) -> Resource:
+          ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+#### Changes to Custom Policies
+This guide leads you through the migration of a custom policy step by step.
+
+**Instantiating a Policy for Training**
+
+Policies are no longer instantiated via their constructor. Instead, all policies have
+to implement a `create` method. During the policy instantiation the configuration from
+the [model configuration](model-configuration.mdx) is passed in as a dictionary instead
+of as separate parameters. Similarly, the`featurizers` are no longer instantiated
+outside of policies.
+Instead, the super class `rasa.core.policies.policy.Policy` instantiates the
+featurizers itself.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-init" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def __init__(
+          self,
+          featurizer: Optional[TrackerFeaturizer] = None,
+          priority: int = DEFAULT_POLICY_PRIORITY,
+          max_history: Optional[int] = None,
+      ) -> None:
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      def __init__(
+          self,
+          featurizer: Optional[TrackerFeaturizer] = None,
+          priority: int = DEFAULT_POLICY_PRIORITY,
+          max_history: Optional[int] = None,
+      ) -> None:
+          super().__init__(
+            config, model_storage, resource, execution_context
+          )
+      ...
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> MyPolicy:
+        return cls(config, model_storage, resource, execution_context)
+
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Persisting a Trained Policy**
+
+Policies used to be persisted by an outside call to the policy's `persist` method.
+With Rasa Open Source 3.0 policies are responsible for persisting themselves.
+Use the provided `model_storage` and `resource`
+(see [graph component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist your graph component at the end of the training and then return the `resource`
+as result of your policy's `train` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-persistence" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def persist(self, path: Union[Text, Path]) -> None:
+          if self.featurizer is not None:
+              self.featurizer.persist(path)
+
+          file_path = Path(path) / "model_data.json"
+          rasa.shared.utils.io.create_directory_for_file(file_path)
+          rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+              self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          **kwargs: Any,
+      ) -> Resource:
+          ...
+          self.persist()
+          return self._resource
+
+      def persist() -> None:
+          with self._model_storage.write_to(self._resource) as directory:
+              if self.featurizer is not None:
+                  self.featurizer.persist(directory)
+
+              file_path = directory / "model_data.json"
+              rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+                  self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Instantiating a Trained Policy**
+
+Similar as you had to use the `model_storage` and `resource` to persist the policy
+in the previous step, you need to use `model_storage` and `resource` in your policy's
+`load` method to instantiate it.
+
+:::info
+It is no longer required to persist your policy's configuration yourself. The config passed
+into `load` will automatically contain the configuration which your model was trained with.
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-loading" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      @classmethod
+      def load(cls, path: Union[Text, Path], **kwargs: Any) -> "Policy":
+          featurizer = None
+          if (Path(path) / FEATURIZER_FILE).is_file():
+              featurizer = TrackerFeaturizer.load(path)
+
+          model_data = {}
+          model_data_file = Path(path) / "model_data.json"
+          if metadata_file.is_file():
+              model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          return cls(model_data, featurizer)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      @classmethod
+      def load(
+          cls,
+          config: Dict[Text, Any],
+          model_storage: ModelStorage,
+          resource: Resource,
+          execution_context: ExecutionContext,
+          **kwargs: Any,
+      ) -> MyPolicy:
+          featurizer = None
+          model_data = {}
+
+          try:
+              with model_storage.read_from(resource) as path:
+                  if (Path(path) / FEATURIZER_FILE).is_file():
+                      featurizer = TrackerFeaturizer.load(path)
+
+                  model_data_file = directory / "model_data.json"
+                  model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          except (ValueError, FileNotFoundError, FileIOException):
+              logger.debug(
+                  f"Couldn't load metadata for policy '{cls.__name__}' as the persisted "
+                  f"metadata couldn't be loaded."
+              )
+
+          return cls(
+              config, model_storage, resource, execution_context,
+              featurizer=featurizer, model_data=model_data
+          )
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Providing a Default Configuration for a Policy**
+
+The default configuration is no longer provided via default values in your policy's
+constructor but instead returned by the static method `get_default_config`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-default-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def __init__(key1: Text = "value1") -> None:
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def __init__(self, config: Dict[Text, Any]) -> None:
+          ...
+
+      @staticmethod
+      def get_default_config() -> Dict[Text, Any]:
+          return {"key1": "value1"}
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Using End-To-End Features in a Policy**
+
+If you were using a custom [end-to-end policy](stories.mdx#end-to-end-training) in Rasa
+Open Source 2, you had to use the `interpreter` parameter to featurize the tracker
+events manually. If your policy requires end-to-end features in Rasa Open Source 3
+you need to [register](custom-graph-components.mdx#registering-graph-components-with-the-model-configuration)
+your policy with type `ComponentType.POLICY_WITH_END_TO_END_SUPPORT`. The features
+will be precomputed and passed into your policy during training and inference.
+
+:::caution
+End-To-End features will only be computed and provided to your policy if your training
+data actually contains [end-to-end training data](stories.mdx#end-to-end-training).
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-end-to-end-features" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> PolicyPrediction:
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {2,9,17}
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.POLICY_WITH_END_TO_END_SUPPORT, is_trainable=True
+  )
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          precomputations: Optional[MessageContainerForCoreFeaturization] = None,
+      ) -> Resource:
+          ...
+
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          precomputations: Optional[MessageContainerForCoreFeaturization] = None,
+          rule_only_data: Optional[Dict[Text, Any]] = None,
+          **kwargs: Any,
+      ) -> PolicyPrediction:
+          ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Registering a Policy**
+
+Before you can use your custom policy you have to register your policy using the
+`DefaultV1Recipe.register` decorator. If your policy requires end-to-end features
+specify the graph component type `POLICY_WITH_END_TO_END_SUPPORT`. Otherwise, use
+`POLICY_WITHOUT_END_TO_END_SUPPORT`. Specify `is_trainable=True` if the `train`
+method of your policy should be called during the training. If your policy is only
+used during inference use `is_trainable=False`.
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-register" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {3-5}
+  from rasa.engine.recipes.default_recipe import DefaultV1Recipe
+
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.POLICY_WITH_END_TO_END_SUPPORT,
+      is_trainable=True
+  )
+  class MyPolicy(Policy):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Providing Rule-only Data to a Policy**
+
+Rasa Open Source allows excluding conversation parts which are completely handled by
+[rules](rules.mdx). In Rasa Open Source 2 the necessary information was passed onto the
+policies using the `set_shared_policy_states` method which set the policy attribute
+`_rule_only_data`. Rasa Open Source passes this data in via the
+`predict_action_probabilities` method. The passed `rule_only_data` can be `None`
+in case the [`RulePolicy`](policies.mdx#rule-policy) is not part of your model
+configuration.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-rule-only-data" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def set_shared_policy_states(self, **kwargs: Any) -> None:
+          """Sets policy's shared states for correct featurization."""
+          self._rule_only_data = kwargs.get("rule_only_data", {})
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {6}
+  class MyPolicy(Policy):
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          rule_only_data: Optional[Dict[Text, Any]] = None,
+    ) -> PolicyPrediction:
+  ```
+
+  </TabItem>
+</Tabs>
+
+#### Changes to Custom NLU Components
+
+**Inheriting from `GraphComponent`**
+
+NLU components which previously inherited from one of the following classes additionally
+need to inherit from the
+[`GraphComponent` interface](custom-graph-components.mdx#the-graphcomponent-interface):
+- `SparseFeaturizer`
+- `DenseFeaturizer`
+- `IntentClassifier`
+- `EntityExtractor`
+
+This snippet shows the required changes:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-parent-class" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(SparseFeaturizer):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {3}
+  from rasa.engine.graph import GraphComponent
+
+  class MyNLUComponent(GraphComponent, SparseFeaturizer):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+** Inheriting from `EntityExtractorMixin` instead of `EntityExtractor`**
+
+The `EntityExtractor` class was renamed to `EntityExtractorMixing`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-entity-extractor" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  from rasa.nlu.extractors.extractor import EntityExtractor
+
+  class MyNLUComponent(EntityExtractor):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {2,4}
+  from rasa.engine.graph import GraphComponent
+  from rasa.nlu.extractors.extractor import EntityExtractorMixin
+
+  class MyNLUComponent(GraphComponent, EntityExtractorMixin):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+
+**Instantiating a NLU Component for Training**
+
+NLU components are no longer instantiated via their constructor. Instead, all NLU
+components have to override the `create` method of the
+[`GraphComponent` interface](custom-graph-components.mdx#the-graphcomponent-interface). The
+passed in configuration is your NLU component's default configuration including any updates
+from your model configuration file.
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-init" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+
+    def __init__(self, component_config: Optional[Dict[Text, Any]] = None) -> None:
+        super().__init__(component_config)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+
+    def __init__(self, component_config: Dict[Text, Any]) -> None:
+        self.component_config = component_config
+      ...
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> GraphComponent:
+        return cls(config)
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Persisting a Trained NLU Component**
+
+NLU components used to be persisted by an outside call to the NLU component's `persist` method.
+With Rasa Open Source 3.0 NLU components are responsible for persisting themselves.
+You should use the provided `model_storage` and `resource`
+(see [component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist your NLU component at the end of the training and then return the `resource`
+as result of your NLU component's `train` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-persistence" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      def train(
+          self,
+          training_data: TrainingData,
+          config: Optional[RasaNLUModelConfig] = None,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def persist(self, file_name: Text, model_dir: Text) -> Dict[Text, Any]:
+          file_path = Path(model_dir) / "{file_name}.model_data.json"
+          rasa.shared.utils.io.create_directory_for_file(file_path)
+          rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+              self.get_model_data())
+
+          return {"file": file_name}
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      def train(self, training_data: TrainingData) -> Resource:
+          ...
+          self.persist()
+          return self._resource
+
+      def persist() -> None:
+          with self._model_storage.write_to(self._resource) as directory:
+              model_data_file = directory / "model_data.json"
+              rasa.shared.utils.io.dump_obj_as_json_to_file(model_data_file,
+                  self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+
+**Instantiating a Trained NLU Component**
+
+Similar as you had to use the `model_storage` and `resource`
+(see [component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist the NLU
+component in the previous step, you need to use these to load the persisted
+NLU component.
+
+:::info
+It is no longer required to persist your NLU component's config yourself. The config passed
+into `load` will contain the configuration which your model was trained with.
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-loading" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      @classmethod
+      def load(
+          cls,
+          meta: Dict[Text, Any],
+          model_dir: Text,
+          model_metadata: Metadata = None,
+          cached_component: Optional["DIETClassifier"] = None,
+          should_finetune: bool = False,
+          **kwargs: Any,
+      ) -> "MyNLUComponent":
+          file_name = meta.get("file")
+          file_path = Path(model_dir) / "{file_name}.model_data.json"
+          model_data = json.loads(rasa.shared.utils.io.read_file(file_path))
+
+          return cls(model_data)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      @classmethod
+      def load(
+          cls,
+          config: Dict[Text, Any],
+          model_storage: ModelStorage,
+          resource: Resource,
+          execution_context: ExecutionContext,
+          **kwargs: Any,
+      ) -> MyNLUComponent:
+          model_data = {}
+
+          try:
+              with model_storage.read_from(resource) as path:
+
+                  model_data_file = directory / "model_data.json"
+                  model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          except (ValueError, FileNotFoundError, FileIOException):
+              logger.debug(
+                  f"Couldn't load metadata for component '{cls.__name__}' as the persisted "
+                  f"model data couldn't be loaded."
+              )
+
+          return cls(
+              config, model_data=model_data
+          )
+  ```
+  </TabItem>
+</Tabs>
+
+**Providing a Default Configuration for an NLU Component**
+
+The default configuration is no longer a static class property but instead returned
+by the static method `get_default_config`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-default-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      ...
+      defaults = {"key1": "value1"}
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      ...
+      @staticmethod
+      def get_default_config() -> Dict[Text, Any]:
+          return {"key1": "value1"}
+
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Augmenting Training Data in an NLU Component**
+
+NLU Components like [tokenizers](components.mdx#tokenizers) or
+[featurizers](components.mdx#featurizers) augment the training data with their
+output during the model training. Their output is required by NLU components later in the
+pipeline. Typically, featurizers require *tokenized* messages as well as intent
+classifiers require *featurized* training data to train themselves. Rasa Open Source
+3.0 makes these different purposes explicit. Previously both NLU component training and
+training data augmentation were done as part of the `train` method. In Rasa Open Source
+3.0 they are split in `train` and `process_training_data`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-component-train-process-split" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(SparseFeaturizer):
+      def train(
+          self,
+          training_data: TrainingData,
+          config: Optional[RasaNLUModelConfig] = None,
+          **kwargs: Any,
+      ) -> None:
+          self.train_featurizer(training_data)
+
+          for message in training_data.training_examples:
+              self.add_features(message)
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, SparseFeaturizer):
+      def train(self, training_data: TrainingData) -> Resource:
+          self.train_featurizer(training_data)
+
+          self.persist()
+          return self._resource
+
+      def process_training_data(training_data: TrainingData) -> TrainingData:
+          for message in training_data.training_examples:
+              self.add_features(message)
+
+          return training_data
+  ```
+  </TabItem>
+</Tabs>
+
+**Handling Lists of Messages During Inference in an NLU Component**
+
+NLU components used to receive a single `Message` object during inference.
+Starting with Rasa Open Source 3.0 all NLU components have to support a list of
+messages during inference. Unless your model supports batch predictions the easiest
+way to handle this is to loop over the messages. It is also required to return the
+message objects at the end of the `process` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-component-process-messages-split" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      def process(self, message: Message, **kwargs: Any) -> None:
+        self.predict(message)
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      def process(self, messages: List[Message]) -> List[Message]:
+          for message in messages:
+              self.predict(message)
+
+          return messages
+  ```
+  </TabItem>
+</Tabs>
+
+
+**Registering your NLU Component**
+
+Before you can use your custom NLU component you have to register your NLU component using the
+`DefaultV1Recipe.register` decorator. The NLU component types correspond to the existing
+parent classes:
+
+* `Tokenizer`: `ComponentType.MESSAGE_TOKENIZER`
+* `SparseFeaturizer` / `DenseFeaturizer`: `ComponentType.MESSAGE_FEATURIZER`
+* `IntentClassifier`: `ComponentType.INTENT_CLASSIFIER`
+* `EntityExtractor`: `ComponentType.ENTITY_EXTRACTOR`
+* If your NLU component provides a pretrained model which then should be used by other
+  NLU components during training and inference use `ComponentType.MODEL_LOADER`
+
+Specify `is_trainable=True` if the `train` method of your policy should be called
+during the training.
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-register" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {3-5}
+  from rasa.engine.recipes.default_recipe import DefaultV1Recipe
+
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER, is_trainable=True
+  )
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Using a Model Provider With your NLU Component**
+
+If your NLU component requires a pretrained model such as a [Spacy](components.mdx#spacynlp) or
+[Mitie](components.mdx#mitienlp) language model you have to specify the NLU component which
+provides this model in your model's pipeline before the NLU component which requires
+the model. In addition to this you now also need to specify the required model in the `model_from`
+parameter in the `register` decorator. The model will then be passed to your model's
+`train`, `process_training_data` and `process` methods:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-model-provider" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      def train(
+          self,
+          training_data: TrainingData,
+          cfg: Optional[RasaNLUModelConfig] = None,
+          **kwargs: Any,
+      ) -> None:
+          """Train the featurizer.
+          spacy_nlp = kwargs.get("spacy_nlp")
+          ...
+
+      def process(self, message: Message, **kwargs: Any) -> None:
+          spacy_nlp = kwargs.get("spacy_nlp", None)
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER, is_trainable=True, model_from="SpacyNLP"
+  )
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      def train(
+          self, training_data: TrainingData, model: SpacyModel) -> Resource:
+          spacy_nlp = model.model
+          ...
+
+      def process(self, messages: List[Message], model: SpacyModel) -> List[Message]:
+          spacy_nlp = model.model
+          ...
+  ```
+
+  </TabItem>
+</Tabs>
+
 ### Training data
 
 `TrainingDataImporter` and all its implementations are updated to contain only synchronous methods.
diff --git a/docs/docs/policies.mdx b/docs/docs/policies.mdx
index cc6e26de7f11..cf2fc828c0c5 100644
--- a/docs/docs/policies.mdx
+++ b/docs/docs/policies.mdx
@@ -1117,8 +1117,19 @@ The featurized trackers will be identical to each other but the target labels in
 
 ## Custom Policies
 
+:::info New in 3.0
+Rasa Open Source 3.0 unified the implementation of NLU components and policies.
+This requires changes to custom policies written for earlier versions of Rasa Open
+Source. Please see the
+[migration guide](migration-guide.mdx#custom-policies-and-custom-components) for a
+step-by-step guide for the migration.
+
+:::
+
+
 You can also write custom policies and reference them in your configuration. In the example below, the
 last two lines show how to use a custom policy class and pass arguments to it.
+See the [guide on custom components](custom-graph-components.mdx) for a full guide on this.
 
 ```yaml-rasa {9-10}
 policies:
@@ -1129,3 +1140,4 @@ policies:
   - name: "path.to.your.policy.class"
     arg1: "..."
 ```
+
diff --git a/docs/sidebars.js b/docs/sidebars.js
index b50cd73a9a3d..a3068dd9128f 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -62,6 +62,7 @@ module.exports = {
             'model-configuration',
             'components',
             'policies',
+            'custom-graph-components',
             'training-data-importers',
             'language-support',
           ],
diff --git a/rasa/engine/validation.py b/rasa/engine/validation.py
index 214530f3fc2a..e0a6f1eff03e 100644
--- a/rasa/engine/validation.py
+++ b/rasa/engine/validation.py
@@ -33,6 +33,7 @@
 from rasa.engine.constants import RESERVED_PLACEHOLDERS
 from rasa.engine.storage.resource import Resource
 from rasa.engine.storage.storage import ModelStorage
+from rasa.shared.constants import DOCS_URL_GRAPH_COMPONENTS
 from rasa.shared.exceptions import RasaException
 from rasa.shared.nlu.training_data.message import Message
 
@@ -147,7 +148,8 @@ def _validate_target(
             f"Your {target_type} model's output component "
             f"'{target_node.uses.__name__}' returns an invalid return "
             f"type '{target_return_type}'. This is not allowed. The {target_type} "
-            f"model's last component is expected to return the type '{expected_type}'."
+            f"model's last component is expected to return the type '{expected_type}'. "
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
 
@@ -188,7 +190,8 @@ def _validate_interface_usage(node: SchemaNode) -> None:
             f"This class does not implement the '{GraphComponent.__name__}' interface "
             f"and can hence not be run within Rasa Open Source. Please use a different "
             f"component or implement the '{GraphComponent}' interface in class "
-            f"'{node.uses.__name__}'."
+            f"'{node.uses.__name__}'. "
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
 
@@ -281,6 +284,7 @@ def _get_type_hints(
             f"all type annotations can be resolved during runtime. Note that you might "
             f"need to do a 'from __future__ import annotations' to avoid forward "
             f"references."
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
 
@@ -293,6 +297,7 @@ def _get_fn(uses: Type[GraphComponent], method_name: Text) -> Callable:
             f"method '{method_name}'. Please make sure you're either using "
             f"the right component or that your component is registered with the "
             f"correct component type."
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
     return fn
 
@@ -314,6 +319,7 @@ def _validate_run_fn(
                 f"'{node.fn}'. Please make sure that you registered "
                 f"your component correctly and and that your model configuration is "
                 f"valid."
+                f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
             )
 
 
@@ -337,6 +343,7 @@ def _validate_run_fn_return_type(
             f"method '{node.fn}' does not have a type annotation for "
             f"its return value. Type annotations are required for all "
             f"components to validate your model's structure."
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
     # TODO: Handle forward references here
@@ -366,6 +373,7 @@ def _validate_types_of_reserved_keywords(
                     f"incompatible type '{param.type_annotation}' for "
                     f"the '{param_name}' parameter in its '{fn_name}' method. "
                     f"The expected type is '{KEYWORDS_EXPECTED_TYPES[param_name]}'."
+                    f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
                 )
 
 
@@ -383,6 +391,7 @@ def _validate_constructor(
             f"('{', '.join(required_args)}'). "
             f"Extra parameters can only be supplied to the constructor method which is "
             f"used during training."
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
     for param_name in _required_args(create_fn_params):
@@ -393,6 +402,7 @@ def _validate_constructor(
                 f"'{node.constructor_name}'. Please make sure that you registered "
                 f"your component correctly and and that your model configuration is "
                 f"valid."
+                f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
             )
 
 
@@ -416,6 +426,7 @@ def _validate_needs(
                 f"name. Please make sure that you registered "
                 f"your component correctly and and that your model configuration is "
                 f"valid."
+                f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
             )
 
         if not _is_placeholder_input(parent_name) and parent_name not in graph.nodes:
@@ -425,6 +436,7 @@ def _validate_needs(
                 f"your model configuration. Please make sure that you registered "
                 f"your component correctly and and that your model configuration is "
                 f"valid."
+                f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
             )
 
         required_type = available_args.get(param_name)
@@ -481,6 +493,7 @@ def _validate_parent_return_type(
             f"Please make sure that you registered "
             f"your component correctly and and that your model configuration is "
             f"valid."
+            f"See {DOCS_URL_GRAPH_COMPONENTS} for more information."
         )
 
 
diff --git a/rasa/shared/constants.py b/rasa/shared/constants.py
index eae0b2b1471b..41f2c22b2ad7 100644
--- a/rasa/shared/constants.py
+++ b/rasa/shared/constants.py
@@ -21,6 +21,7 @@
 DOCS_URL_PIKA_EVENT_BROKER = DOCS_URL_EVENT_BROKERS + "#pika-event-broker"
 DOCS_URL_TRACKER_STORES = DOCS_BASE_URL + "/tracker-stores"
 DOCS_URL_COMPONENTS = DOCS_BASE_URL + "/components"
+DOCS_URL_GRAPH_COMPONENTS = DOCS_BASE_URL + "/custom-graph-components"
 DOCS_URL_MIGRATION_GUIDE = DOCS_BASE_URL + "/migration-guide"
 DOCS_URL_MIGRATION_GUIDE_MD_DEPRECATION = (
     f"{DOCS_URL_MIGRATION_GUIDE}#rasa-21-to-rasa-22"
diff --git a/tests/docs/test_docs_embedded_modules.py b/tests/docs/test_docs_embedded_modules.py
new file mode 100644
index 000000000000..a7fb1877bfca
--- /dev/null
+++ b/tests/docs/test_docs_embedded_modules.py
@@ -0,0 +1,18 @@
+import inspect
+
+from rasa.engine.graph import GraphComponent
+from data.test_classes.graph_component_interface import (
+    GraphComponent as GraphComponentDocs,
+)
+
+
+def test_graph_copy_does_not_diverge():
+    """Tests that the module embedded in the docs doesn't diverge from the actual one.
+
+    It is currently not possible to embed a class from within a module in the docs
+    without embedding the rest of the module file. To avoid this we have copied the
+    `GraphComponent` to a separate file which is embeded in the docs.
+    """
+    # If this fails then copy the latest implementation of
+    # `rasa.engine.graph.GraphComponent` to `data.test_classes.graph_component_interface
+    assert inspect.getsource(GraphComponent) == inspect.getsource(GraphComponentDocs)


Code review comments:
diff_hunk: @@ -24,6 +26,904 @@ If you still have training data in Markdown format then the recommended approach
 to convert your data from Markdown to YAML. Please use the commands described
 [here](./migration-guide.mdx#training-data-files).
 
+### Model Configuration
+
+It is required to specify the used `recipe` within the
+[model configuration](model-configuration.mdx). As of now Rasa Open Source only supports
+the `default.v1` recipe and will continue using this even if you don't specify this
+in the model configuration. To avoid breaking changes in the future it is recommended
+to specify `recipe: "default.v1"` at the top of your model configuration:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```yaml-rasa
+  language: en
+
+  pipeline:
+    ...
+  policies:
+    ...
+  ```
+  </TabItem>
+  <TabItem value="new">
+
+  ```yaml-rasa {1}
+  recipe: default.v1
+
+  language: en
+
+  pipeline:
+    ...
+  policies:
+    ...
+  ```
+  </TabItem>
+</Tabs>
+
+### Custom Policies and Custom Components
+
+Rasa Open Source 3.0 changed the way how [NLU components](components.mdx) and
+[policies](policies.mdx) are trained and run during inference. As part of these changes
+the interfaces for NLU components and policies have been unified and adapted.
+
+The next sections outline which adaptions are required to run your custom NLU components
+and policies with Rasa Open Source 3.0.
+
+:::caution
+
+Please read the updated guide on custom graph components [here](custom-graph-components.mdx) before
+continuing to follow the step-by-step guide to migrate your own custom graph components.
+:::
+
+**Type Annotations**
+
+Until Rasa Open Source 3.0 [type annotations](https://docs.python.org/3/library/typing.html)
+were not required in custom policies or custom NLU components.
+It is now required to use
+[type annotations](https://docs.python.org/3/library/typing.html) in custom NLU
+components and policies. Rasa Open Source uses these type annotations to validate that
+your graph components are compatible and correctly configured. As outlined in the custom
+[components guide](custom-graph-components.mdx) it is not allowed to use
+[Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references).
+
+:::info Forward References with Python 3.7
+Use the
+[`from __future__ import annotations`](https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7)
+import to avoid using forward references with Python 3.7.
+:::
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-type-annotations" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List["TrackerWithCachedStates"],
+          domain,
+          **kwargs: Any,
+      ):
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  from __future__ import annotations
+
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          **kwargs: Any,
+      ) -> Resource:
+          ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+#### Changes to Custom Policies
+This guide leads you through the migration of a custom policy step by step.
+
+**Instantiating a Policy for Training**
+
+Policies are no longer instantiated via their constructor. Instead, all policies have
+to implement a `create` method. During the policy instantiation the configuration from
+the [model configuration](model-configuration.mdx) is passed in as a dictionary instead
+of as separate parameters. Similarly, the`featurizers` are no longer instantiated
+outside of policies.
+Instead, the super class `rasa.core.policies.policy.Policy` instantiates the
+featurizers itself.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-init" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def __init__(
+          self,
+          featurizer: Optional[TrackerFeaturizer] = None,
+          priority: int = DEFAULT_POLICY_PRIORITY,
+          max_history: Optional[int] = None,
+      ) -> None:
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      def __init__(
+          self,
+          featurizer: Optional[TrackerFeaturizer] = None,
+          priority: int = DEFAULT_POLICY_PRIORITY,
+          max_history: Optional[int] = None,
+      ) -> None:
+          super().__init__(
+            config, model_storage, resource, execution_context
+          )
+      ...
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> MyPolicy:
+        return cls(config, model_storage, resource, execution_context)
+
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Persisting a Trained Policy**
+
+Policies used to be persisted by an outside call to the policy's `persist` method.
+With Rasa Open Source 3.0 policies are responsible for persisting themselves.
+Use the provided `model_storage` and `resource`
+(see [graph component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist your graph component at the end of the training and then return the `resource`
+as result of your policy's `train` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-persistence" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def persist(self, path: Union[Text, Path]) -> None:
+          if self.featurizer is not None:
+              self.featurizer.persist(path)
+
+          file_path = Path(path) / "model_data.json"
+          rasa.shared.utils.io.create_directory_for_file(file_path)
+          rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+              self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          **kwargs: Any,
+      ) -> Resource:
+          ...
+          self.persist()
+          return self._resource
+
+      def persist() -> None:
+          with self._model_storage.write_to(self._resource) as directory:
+              if self.featurizer is not None:
+                  self.featurizer.persist(directory)
+
+              file_path = directory / "model_data.json"
+              rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+                  self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Instantiating a Trained Policy**
+
+Similar as you had to use the `model_storage` and `resource` to persist the policy
+in the previous step, you need to use `model_storage` and `resource` in your policy's
+`load` method to instantiate it.
+
+:::info
+It is no longer required to persist your policy's configuration yourself. The config passed
+into `load` will automatically contain the configuration which your model was trained with.
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-loading" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      @classmethod
+      def load(cls, path: Union[Text, Path], **kwargs: Any) -> "Policy":
+          featurizer = None
+          if (Path(path) / FEATURIZER_FILE).is_file():
+              featurizer = TrackerFeaturizer.load(path)
+
+          model_data = {}
+          model_data_file = Path(path) / "model_data.json"
+          if metadata_file.is_file():
+              model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          return cls(model_data, featurizer)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+      @classmethod
+      def load(
+          cls,
+          config: Dict[Text, Any],
+          model_storage: ModelStorage,
+          resource: Resource,
+          execution_context: ExecutionContext,
+          **kwargs: Any,
+      ) -> MyPolicy:
+          featurizer = None
+          model_data = {}
+
+          try:
+              with model_storage.read_from(resource) as path:
+                  if (Path(path) / FEATURIZER_FILE).is_file():
+                      featurizer = TrackerFeaturizer.load(path)
+
+                  model_data_file = directory / "model_data.json"
+                  model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          except (ValueError, FileNotFoundError, FileIOException):
+              logger.debug(
+                  f"Couldn't load metadata for policy '{cls.__name__}' as the persisted "
+                  f"metadata couldn't be loaded."
+              )
+
+          return cls(
+              config, model_storage, resource, execution_context,
+              featurizer=featurizer, model_data=model_data
+          )
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Providing a Default Configuration for a Policy**
+
+The default configuration is no longer provided via default values in your policy's
+constructor but instead returned by the static method `get_default_config`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-default-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def __init__(key1: Text = "value1") -> None:
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def __init__(self, config: Dict[Text, Any]) -> None:
+          ...
+
+      @staticmethod
+      def get_default_config() -> Dict[Text, Any]:
+          return {"key1": "value1"}
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Using End-To-End Features in a Policy**
+
+If you were using a custom [end-to-end policy](stories.mdx#end-to-end-training) in Rasa
+Open Source 2, you had to use the `interpreter` parameter to featurize the tracker
+events manually. If your policy requires end-to-end features in Rasa Open Source 3
+you need to [register](custom-graph-components.mdx#registering-graph-components-with-the-model-configuration)
+your policy with type `ComponentType.POLICY_WITH_END_TO_END_SUPPORT`. The features
+will be precomputed and passed into your policy during training and inference.
+
+:::caution
+End-To-End features will only be computed and provided to your policy if your training
+data actually contains [end-to-end training data](stories.mdx#end-to-end-training).
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-end-to-end-features" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          interpreter: NaturalLanguageInterpreter,
+          **kwargs: Any,
+      ) -> PolicyPrediction:
+          ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {2,9,17}
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.POLICY_WITH_END_TO_END_SUPPORT, is_trainable=True
+  )
+  class MyPolicy(Policy):
+      def train(
+          self,
+          training_trackers: List[TrackerWithCachedStates],
+          domain: Domain,
+          precomputations: Optional[MessageContainerForCoreFeaturization] = None,
+      ) -> Resource:
+          ...
+
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          precomputations: Optional[MessageContainerForCoreFeaturization] = None,
+          rule_only_data: Optional[Dict[Text, Any]] = None,
+          **kwargs: Any,
+      ) -> PolicyPrediction:
+          ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Registering a Policy**
+
+Before you can use your custom policy you have to register your policy using the
+`DefaultV1Recipe.register` decorator. If your policy requires end-to-end features
+specify the graph component type `POLICY_WITH_END_TO_END_SUPPORT`. Otherwise, use
+`POLICY_WITHOUT_END_TO_END_SUPPORT`. Specify `is_trainable=True` if the `train`
+method of your policy should be called during the training. If your policy is only
+used during inference use `is_trainable=False`.
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-register" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {3-5}
+  from rasa.engine.recipes.default_recipe import DefaultV1Recipe
+
+  @DefaultV1Recipe.register(
+      DefaultV1Recipe.ComponentType.POLICY_WITH_END_TO_END_SUPPORT,
+      is_trainable=True
+  )
+  class MyPolicy(Policy):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Providing Rule-only Data to a Policy**
+
+Rasa Open Source allows excluding conversation parts which are completely handled by
+[rules](rules.mdx). In Rasa Open Source 2 the necessary information was passed onto the
+policies using the `set_shared_policy_states` method which set the policy attribute
+`_rule_only_data`. Rasa Open Source passes this data in via the
+`predict_action_probabilities` method. The passed `rule_only_data` can be `None`
+in case the [`RulePolicy`](policies.mdx#rule-policy) is not part of your model
+configuration.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-policy-rule-only-data" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyPolicy(Policy):
+
+      def set_shared_policy_states(self, **kwargs: Any) -> None:
+          """Sets policy's shared states for correct featurization."""
+          self._rule_only_data = kwargs.get("rule_only_data", {})
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {6}
+  class MyPolicy(Policy):
+      def predict_action_probabilities(
+          self,
+          tracker: DialogueStateTracker,
+          domain: Domain,
+          rule_only_data: Optional[Dict[Text, Any]] = None,
+    ) -> PolicyPrediction:
+  ```
+
+  </TabItem>
+</Tabs>
+
+#### Changes to Custom NLU Components
+
+**Inheriting from `GraphComponent`**
+
+NLU components which previously inherited from one of the following classes additionally
+need to inherit from the
+[`GraphComponent` interface](custom-graph-components.mdx#the-graphcomponent-interface):
+- `SparseFeaturizer`
+- `DenseFeaturizer`
+- `IntentClassifier`
+- `EntityExtractor`
+
+This snippet shows the required changes:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-parent-class" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(SparseFeaturizer):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {3}
+  from rasa.engine.graph import GraphComponent
+
+  class MyNLUComponent(GraphComponent, SparseFeaturizer):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+** Inheriting from `EntityExtractorMixin` instead of `EntityExtractor`**
+
+The `EntityExtractor` class was renamed to `EntityExtractorMixing`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-entity-extractor" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  from rasa.nlu.extractors.extractor import EntityExtractor
+
+  class MyNLUComponent(EntityExtractor):
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python {2,4}
+  from rasa.engine.graph import GraphComponent
+  from rasa.nlu.extractors.extractor import EntityExtractorMixin
+
+  class MyNLUComponent(GraphComponent, EntityExtractorMixin):
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+
+**Instantiating a NLU Component for Training**
+
+NLU components are no longer instantiated via their constructor. Instead, all NLU
+components have to override the `create` method of the
+[`GraphComponent` interface](custom-graph-components.mdx#the-graphcomponent-interface). The
+passed in configuration is your NLU component's default configuration including any updates
+from your model configuration file.
+
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-init" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+
+    def __init__(self, component_config: Optional[Dict[Text, Any]] = None) -> None:
+        super().__init__(component_config)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+
+    def __init__(self, component_config: Dict[Text, Any]) -> None:
+        self.component_config = component_config
+      ...
+
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> GraphComponent:
+        return cls(config)
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Persisting a Trained NLU Component**
+
+NLU components used to be persisted by an outside call to the NLU component's `persist` method.
+With Rasa Open Source 3.0 NLU components are responsible for persisting themselves.
+You should use the provided `model_storage` and `resource`
+(see [component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist your NLU component at the end of the training and then return the `resource`
+as result of your NLU component's `train` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-persistence" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      def train(
+          self,
+          training_data: TrainingData,
+          config: Optional[RasaNLUModelConfig] = None,
+          **kwargs: Any,
+      ) -> None:
+          ...
+
+      def persist(self, file_name: Text, model_dir: Text) -> Dict[Text, Any]:
+          file_path = Path(model_dir) / "{file_name}.model_data.json"
+          rasa.shared.utils.io.create_directory_for_file(file_path)
+          rasa.shared.utils.io.dump_obj_as_json_to_file(file_path,
+              self.get_model_data())
+
+          return {"file": file_name}
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      def train(self, training_data: TrainingData) -> Resource:
+          ...
+          self.persist()
+          return self._resource
+
+      def persist() -> None:
+          with self._model_storage.write_to(self._resource) as directory:
+              model_data_file = directory / "model_data.json"
+              rasa.shared.utils.io.dump_obj_as_json_to_file(model_data_file,
+                  self.get_model_data())
+      ...
+  ```
+
+  </TabItem>
+</Tabs>
+
+
+**Instantiating a Trained NLU Component**
+
+Similar as you had to use the `model_storage` and `resource`
+(see [component persistence](custom-graph-components.mdx#model-persistence) for more details)
+to persist the NLU
+component in the previous step, you need to use these to load the persisted
+NLU component.
+
+:::info
+It is no longer required to persist your NLU component's config yourself. The config passed
+into `load` will contain the configuration which your model was trained with.
+:::
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-loading" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      @classmethod
+      def load(
+          cls,
+          meta: Dict[Text, Any],
+          model_dir: Text,
+          model_metadata: Metadata = None,
+          cached_component: Optional["DIETClassifier"] = None,
+          should_finetune: bool = False,
+          **kwargs: Any,
+      ) -> "MyNLUComponent":
+          file_name = meta.get("file")
+          file_path = Path(model_dir) / "{file_name}.model_data.json"
+          model_data = json.loads(rasa.shared.utils.io.read_file(file_path))
+
+          return cls(model_data)
+      ...
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      @classmethod
+      def load(
+          cls,
+          config: Dict[Text, Any],
+          model_storage: ModelStorage,
+          resource: Resource,
+          execution_context: ExecutionContext,
+          **kwargs: Any,
+      ) -> MyNLUComponent:
+          model_data = {}
+
+          try:
+              with model_storage.read_from(resource) as path:
+
+                  model_data_file = directory / "model_data.json"
+                  model_data = json.loads(rasa.shared.utils.io.read_file(model_data_file))
+
+          except (ValueError, FileNotFoundError, FileIOException):
+              logger.debug(
+                  f"Couldn't load metadata for component '{cls.__name__}' as the persisted "
+                  f"model data couldn't be loaded."
+              )
+
+          return cls(
+              config, model_data=model_data
+          )
+  ```
+  </TabItem>
+</Tabs>
+
+**Providing a Default Configuration for an NLU Component**
+
+The default configuration is no longer a static class property but instead returned
+by the static method `get_default_config`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-nlu-default-config" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      ...
+      defaults = {"key1": "value1"}
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      ...
+      @staticmethod
+      def get_default_config() -> Dict[Text, Any]:
+          return {"key1": "value1"}
+
+  ```
+
+  </TabItem>
+</Tabs>
+
+**Augmenting Training Data in an NLU Component**
+
+NLU Components like [tokenizers](components.mdx#tokenizers) or
+[featurizers](components.mdx#featurizers) augment the training data with their
+output during the model training. Their output is required by NLU components later in the
+pipeline. Typically, featurizers require *tokenized* messages as well as intent
+classifiers require *featurized* training data to train themselves. Rasa Open Source
+3.0 makes these different purposes explicit. Previously both NLU component training and
+training data augmentation were done as part of the `train` method. In Rasa Open Source
+3.0 they are split in `train` and `process_training_data`:
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-component-train-process-split" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(SparseFeaturizer):
+      def train(
+          self,
+          training_data: TrainingData,
+          config: Optional[RasaNLUModelConfig] = None,
+          **kwargs: Any,
+      ) -> None:
+          self.train_featurizer(training_data)
+
+          for message in training_data.training_examples:
+              self.add_features(message)
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, SparseFeaturizer):
+      def train(self, training_data: TrainingData) -> Resource:
+          self.train_featurizer(training_data)
+
+          self.persist()
+          return self._resource
+
+      def process_training_data(training_data: TrainingData) -> TrainingData:
+          for message in training_data.training_examples:
+              self.add_features(message)
+
+          return training_data
+  ```
+  </TabItem>
+</Tabs>
+
+**Handling Lists of Messages During Inference in an NLU Component**
+
+NLU components used to receive a single `Message` object during inference.
+Starting with Rasa Open Source 3.0 all NLU components have to support a list of
+messages during inference. Unless your model supports batch predictions the easiest
+way to handle this is to loop over the messages. It is also required to return the
+message objects at the end of the `process` method.
+
+<Tabs values={[{"label": "Rasa 2.0 (old)", "value": "old"}, {"label": "Rasa 3.0 (new)", "value": "new"}]}  groupId="3-0-component-process-messages-split" defaultValue="new">
+  <TabItem value="old">
+
+  ```python
+  class MyNLUComponent(IntentClassifier):
+      def process(self, message: Message, **kwargs: Any) -> None:
+        self.predict(message)
+  ```
+
+  </TabItem>
+  <TabItem value="new">
+
+  ```python
+  class MyNLUComponent(GraphComponent, IntentClassifier):
+      def process(self, messages: List[Message]) -> List[Message]:
+          for message in messages:
+              self.predict(message)
+
+          return messages
+  ```
+  </TabItem>
+</Tabs>
+
+
+**Registering your NLU Component**
+
+Before you can use your custom NLU component you have to register your NLU component using the
+`DefaultV1Recipe.register` decorator. The NLU component types correspond to the existing
+parent classes:
+
+* `Tokenizer`: `ComponentType.MESSAGE_TOKENIZER`
+* `SparseFeaturizer` / `DenseFeaturizer`: `ComponentType.MESSAGE_FEATURIZER`
+* `IntentClassifier`: `ComponentType.INTENT_CLASSIFIER`
+* `EntityExtractor`: `ComponentType.ENTITY_EXTRACTOR`
+* If your NLU component provides a pretrained model which then should be used by other
+  NLU components during training and inference use `ComponentType.MODEL_LOADER`
+
+Specify `is_trainable=True` if the `train` method of your policy should be called
path: docs/docs/migration-guide.mdx
text: @user1:
```suggestion
Specify `is_trainable=True` if the `train` method of your component should be called
```

diff_hunk: @@ -0,0 +1,429 @@
+---
+id: custom-graph-components
+sidebar_label: Custom Graph Components
+title: Custom Graph Components
+abstract: You can extend Rasa Open Source with custom NLU components and policies. This page provides a guide on how to develop your own custom graph components.
+---
+
+Rasa Open Source provides a variety of [NLU components](components.mdx) and
+[policies](policies.mdx) out of the box. You can customize them or create your
+own components from scratch by using custom graph components.
+
+To use your custom graph component with Rasa Open Source it has to fulfill the following
+requirements:
+- It has to implement the [`GraphComponent` interface](#the-graphcomponent-interface)
+- It has to be [registered](#registering-graph-components-with-the-model-configuration)
+  with the used model configuration
+- It has to be used in the [configuration file](#using-custom-components-in-your-model-configuration)
+- It has to use type annotations. Rasa Open Source makes use of the type annotations
+  to validate your model configuration.
+  [Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references)
+  are not allowed. If you're using Python 3.7 you can use
+  [`from __future__ import annotations`](https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7)
+  to get rid of forward references.
+
+
+## Graph Components
+
+Rasa Open Source uses the passed in [model configuration](model-configuration.mdx) to build a
+[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
+This graph describes the dependencies between the items in your model configuration and
+how data flows between them. This has two major benefits:
+
+- Rasa Open Source can use the computational graph to optimize the execution of your
+  model. Examples for this are efficient caching of training steps or executing
+  independent steps in parallel.
+- Rasa Open Source can represent different model architectures flexibly. As long as the
+  graph remains acyclic Rasa Open Source can in theory pass any data to any graph
+  component based on the model configuration without having to tie the underlying
+  software architecture to the used model architecture.
+
+When translating the model configuration to the computational graph
+[policies](policies.mdx) and [NLU components](components.mdx) become nodes within this graph.
+While there is a distinction between policies and NLU components in your model
+configuration the distinction is abstracted away when they are placed within the graph.
+At this point policies and NLU components become abstract *graph components*.
+In practice this is represented by the
+[`GraphComponent`](custom-graph-components.mdx#the-graphcomponent-interface)
+interface: Both policies and NLU components have to inherit from this interface to
+become compatible and executable for Rasa's graph.
+
+## Getting Started
+Before you get started you have to decide whether you want to implement a custom
+[NLU component](components.mdx) or a [policy](policies.mdx). If you are implementing
+a custom policy then we recommend extending the existing
+`rasa.core.policies.policy.Policy` class.
+
+```python
+from rasa.core.policies.policy import Policy
+
+# TODO: Correctly register your graph component
+@DefaultV1Recipe.register(
+    [DefaultV1Recipe.ComponentType.POLICY_WITHOUT_END_TO_END_SUPPORT], is_trainable=True
+)
+class MyPolicy(Policy):
+    ...
+```
+
+If you want to implement a custom NLU component then start out with the following
+skeleton:
+
+```python (docs/sources/data/test_classes/nlu_component_skeleton.py)
+```
+:::note custom tokenizers
+If you create a custom tokenizer you should implement the methods of
+`rasa.nlu.tokenizers.tokenizer.Tokenizer`. The `train` and `process` methods are
+already implemented so you only need to overwrite the `tokenize` method.
+
+:::
+
+
+
+## The `GraphComponent` interface
+To run your custom NLU component or policy with Rasa Open Source it has
+to implement the `GraphComponent` interface.
+
+```python (docs/sources/data/test_classes/graph_component_interface.py)
+```
+
+### `create`
+The `create` method is used to instantiate your graph component during training and has to be
+overridden. Rasa Open Source passes the following parameters when calling the method:
+
+- `config`: This is your model's default configuration merged with the
+   configuration provided to the graph component in the model configuration file.
+- `model_storage`: You can use this to persist and load your graph components. See
+   [model persistence](#model-persistence) section for further details on its usage.
+- `resource`: This uniquely identifies your component within the `model_storage`.
+   See the [model persistence](#model-persistence) section for further
+   details on its usage.
+- `execution_context`: This provides additional information about the current
+   mode of execution:
+   - `model_id`: A unique identifier for the model used during inference. This
+      parameter is `None` during training.
+   - `should_add_diagnostic_data`: If `True` then additional diagnostic metadata
+      can be added to your graph component's predictions on top of the actual prediction.
+   - `is_finetuning`: If `True` then the graph component is trained using
+     [finetuning](command-line-interface.mdx#incremental-training).
+   - `graph_schema`: The `graph_schema` describes the computational graph which is used
+     to train your assistant or to make predictions with it.
+   - `node_name`: The `node_name` is a unique identifier for the step in the graph
+     schema which is fulfilled by the called graph component
+
+### `load`
+The `load` method is used to instantiate your graph component during inference. The default
+implementation of this method calls your `create` method. It is recommended to override
+this if your graph component
+[persists data as part of the training](#model-persistence).
+See [`create`](#create) for a description of the single parameters.
+
+### `get_default_config`
+The method `get_default_config` returns the default configuration for your graph
+component. Its default implementation returns an empty dictionary which implies that
+the graph component
+does not have any configuration. Rasa Open Source will update the configuration given
+in the configuration file with the default configuration during runtime.
+
+### `supported_languages`
+The method `supported_languages` specifies which languages a graph component supports.
+Rasa Open Source will use the `language` key in the model configuration file to
+validate that the graph component is valid for usage with the specified language.
+If a graph component returns `None` (this is the default implementation), it indicates
+that the graph component supports all languages which are not part of
+`not_supported_languages`.
+
+   Examples:
+   - `[]`: The graph component does not support any language
+   - `None`: All languages are supported expect the languages defined in
+     `not_supported_languages`
+   - `["en"]`: The graph component can only be used with English conversations.
+
+### `not_supported_languages`
+The method `not_supported_languages` specifies which languages your
+graph component does not support. Rasa Open Source will use the `language` key in the
+model configuration file to validate that your graph component is valid for usage with
+the specified language. If your graph component returns `None` (this is the default
+implementation), you indicate that it supports all languages which are specified in
+`supported_languages`.
+
+   Examples:
+   - `None` or `[]`: All languages specified in `supported_languages` are supported.
+   - `["en"]`: The graph component can be used with any language except English.
+
+### `required_packages`
+The `required_packages` method indicates which Python packages need to
+be installed to be able to use this graph component. Rasa Open Source will raise an error
+during execution if your graoh component is used without the required libraries being
+installed. By default, this method returns an empty list which implies that your graph
+component does not have any extra dependencies.
+
+Examples:
+- `[]`: No extra packages are required to use this graph component
+- `["spacy"]`: The Python package `spacy` needs to be installed to be able to run
+  this graph component.
+
+## Model Persistence
+Some graph components require persisting data during training which should be available
+to the graph component during inference time. A typical use case for this is storing model
+weights. Rasa Open Source provides the
+`model_storage` and `resource` parameters to your graph component's `create` and `load`
+method for this purpose as shown in the snippet below. The `model_storage` provides
+access to data from all graph components. The `resource` allows you to uniquely identify
+your graph component's location in the model storage.
+
+
+```python {6,7,16,17}
+class MyComponent(GraphComponent):
+    @classmethod
+    def create(
+        cls,
+        config: Dict[Text, Any],
+        model_storage: ModelStorage,
+        resource: Resource,
+        execution_context: ExecutionContext,
+    ) -> MyComponent:
+        ...
+
+    @classmethod
+    def create(
path: docs/docs/custom-graph-components.mdx
text: @user1:
```suggestion
    def load(
```

diff_hunk: @@ -0,0 +1,429 @@
+---
+id: custom-graph-components
+sidebar_label: Custom Graph Components
+title: Custom Graph Components
+abstract: You can extend Rasa Open Source with custom NLU components and policies. This page provides a guide on how to develop your own custom graph components.
+---
+
+Rasa Open Source provides a variety of [NLU components](components.mdx) and
+[policies](policies.mdx) out of the box. You can customize them or create your
+own components from scratch by using custom graph components.
+
+To use your custom graph component with Rasa Open Source it has to fulfill the following
+requirements:
+- It has to implement the [`GraphComponent` interface](#the-graphcomponent-interface)
+- It has to be [registered](#registering-graph-components-with-the-model-configuration)
+  with the used model configuration
+- It has to be used in the [configuration file](#using-custom-components-in-your-model-configuration)
+- It has to use type annotations. Rasa Open Source makes use of the type annotations
+  to validate your model configuration.
+  [Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references)
+  are not allowed. If you're using Python 3.7 you can use
+  [`from __future__ import annotations`](https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7)
+  to get rid of forward references.
+
+
+## Graph Components
+
+Rasa Open Source uses the passed in [model configuration](model-configuration.mdx) to build a
+[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
+This graph describes the dependencies between the items in your model configuration and
+how data flows between them. This has two major benefits:
+
+- Rasa Open Source can use the computational graph to optimize the execution of your
+  model. Examples for this are efficient caching of training steps or executing
+  independent steps in parallel.
+- Rasa Open Source can represent different model architectures flexibly. As long as the
+  graph remains acyclic Rasa Open Source can in theory pass any data to any graph
+  component based on the model configuration without having to tie the underlying
+  software architecture to the used model architecture.
+
+When translating the model configuration to the computational graph
+[policies](policies.mdx) and [NLU components](components.mdx) become nodes within this graph.
+While there is a distinction between policies and NLU components in your model
+configuration the distinction is abstracted away when they are placed within the graph.
+At this point policies and NLU components become abstract *graph components*.
+In practice this is represented by the
+[`GraphComponent`](custom-graph-components.mdx#the-graphcomponent-interface)
+interface: Both policies and NLU components have to inherit from this interface to
+become compatible and executable for Rasa's graph.
+
+## Getting Started
+Before you get started you have to decide whether you want to implement a custom
+[NLU component](components.mdx) or a [policy](policies.mdx). If you are implementing
+a custom policy then we recommend extending the existing
+`rasa.core.policies.policy.Policy` class.
+
+```python
+from rasa.core.policies.policy import Policy
+
+# TODO: Correctly register your graph component
+@DefaultV1Recipe.register(
+    [DefaultV1Recipe.ComponentType.POLICY_WITHOUT_END_TO_END_SUPPORT], is_trainable=True
+)
+class MyPolicy(Policy):
+    ...
+```
+
+If you want to implement a custom NLU component then start out with the following
+skeleton:
+
+```python (docs/sources/data/test_classes/nlu_component_skeleton.py)
+```
+:::note custom tokenizers
+If you create a custom tokenizer you should implement the methods of
+`rasa.nlu.tokenizers.tokenizer.Tokenizer`. The `train` and `process` methods are
+already implemented so you only need to overwrite the `tokenize` method.
+
+:::
+
+
+
+## The `GraphComponent` interface
+To run your custom NLU component or policy with Rasa Open Source it has
+to implement the `GraphComponent` interface.
+
+```python (docs/sources/data/test_classes/graph_component_interface.py)
+```
+
+### `create`
+The `create` method is used to instantiate your graph component during training and has to be
+overridden. Rasa Open Source passes the following parameters when calling the method:
+
+- `config`: This is your model's default configuration merged with the
+   configuration provided to the graph component in the model configuration file.
+- `model_storage`: You can use this to persist and load your graph components. See
+   [model persistence](#model-persistence) section for further details on its usage.
+- `resource`: This uniquely identifies your component within the `model_storage`.
+   See the [model persistence](#model-persistence) section for further
+   details on its usage.
+- `execution_context`: This provides additional information about the current
+   mode of execution:
+   - `model_id`: A unique identifier for the model used during inference. This
+      parameter is `None` during training.
+   - `should_add_diagnostic_data`: If `True` then additional diagnostic metadata
+      can be added to your graph component's predictions on top of the actual prediction.
+   - `is_finetuning`: If `True` then the graph component is trained using
+     [finetuning](command-line-interface.mdx#incremental-training).
+   - `graph_schema`: The `graph_schema` describes the computational graph which is used
+     to train your assistant or to make predictions with it.
+   - `node_name`: The `node_name` is a unique identifier for the step in the graph
+     schema which is fulfilled by the called graph component
+
+### `load`
+The `load` method is used to instantiate your graph component during inference. The default
+implementation of this method calls your `create` method. It is recommended to override
+this if your graph component
+[persists data as part of the training](#model-persistence).
+See [`create`](#create) for a description of the single parameters.
+
+### `get_default_config`
+The method `get_default_config` returns the default configuration for your graph
+component. Its default implementation returns an empty dictionary which implies that
+the graph component
+does not have any configuration. Rasa Open Source will update the configuration given
+in the configuration file with the default configuration during runtime.
+
+### `supported_languages`
+The method `supported_languages` specifies which languages a graph component supports.
+Rasa Open Source will use the `language` key in the model configuration file to
+validate that the graph component is valid for usage with the specified language.
+If a graph component returns `None` (this is the default implementation), it indicates
+that the graph component supports all languages which are not part of
+`not_supported_languages`.
+
+   Examples:
+   - `[]`: The graph component does not support any language
+   - `None`: All languages are supported expect the languages defined in
+     `not_supported_languages`
+   - `["en"]`: The graph component can only be used with English conversations.
+
+### `not_supported_languages`
+The method `not_supported_languages` specifies which languages your
+graph component does not support. Rasa Open Source will use the `language` key in the
+model configuration file to validate that your graph component is valid for usage with
+the specified language. If your graph component returns `None` (this is the default
+implementation), you indicate that it supports all languages which are specified in
+`supported_languages`.
+
+   Examples:
+   - `None` or `[]`: All languages specified in `supported_languages` are supported.
+   - `["en"]`: The graph component can be used with any language except English.
+
+### `required_packages`
+The `required_packages` method indicates which Python packages need to
+be installed to be able to use this graph component. Rasa Open Source will raise an error
+during execution if your graoh component is used without the required libraries being
path: docs/docs/custom-graph-components.mdx
text: @user1:
```suggestion
during execution if your graph component is used without the required libraries being
```