diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index 229ae8729dc8808812e13714260fc6564edc193a..b2fda9fe2dd188293ffdf2f012228975ebee68ef 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -1,4 +1,36 @@
+acmymx
+agtxee
+aowp
 automator
 automators
+beihz
+bfoheo
+byzhmc
+Cplain
+cxcwjm
 echoai
+eoahw
 flac
+gguvde
+iwzr
+iztkfs
+jqykgu
+mkdocs
+nagg
+originalentity
+originalentityref
+pgta
+qlvkq
+refentity
+referencable
+Retryable
+rgzuve
+rlgsjy
+Spatie
+tbbhie
+tgic
+vgtd
+webform
+yjwm
+zcaglk
+zpul
diff --git a/README.md b/README.md
index 314942fd5801fdfba993f6a145ba41de04990d95..26fd1d4fb4edf88d212f136fbc26f21542089b81 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,27 @@
 # AI - ECA
+
+Artificial Intelligence integration for Event-Condition-Action module, combining the unified framework
+and the power of Drupal to perform various AI-related operations.
+
+## Dependencies
+
+- Drupal ^10.3 || ^11
+- [AI module](https://drupal.org/project/ai)
+- [ECA module](https://drupal.org/project/eca)
+
+## Getting started
+
+1. Enable the module
+2. Open an ECA model
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeech` etc.) as action
+
+## Documentation
+
+This project uses MkDocs for documentation: you can see the current
+documentation at [https://project.pages.drupalcode.org/ai/](https://project.pages.drupalcode.org/ai/).
+The documentation source files are located in the `docs/` directory, and to
+build your own local version of the documentation please follow these steps:
+
+1. Install MkDocs: `pip install mkdocs mkdocs-material`
+2. Run `mkdocs serve` in the project root
+3. Open `http://localhost:8000` in your browser
diff --git a/composer.json b/composer.json
index 9c66d18aa9ed5a90bc8705ea27828dbadfe605df..9576e17c264f53b256266ddeaacf081ce2648cd0 100644
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,14 @@
     },
     "require": {
         "drupal/ai": "1.0.x-dev@dev",
+        "drupal/ai_agents": "1.0.x-dev@dev",
+        "drupal/core": "^10.3 || ^11",
         "drupal/eca": "^2.0",
-        "drupal/core": "^10.3 || ^11"
+        "drupal/token": "^1.15",
+        "illuminate/support": "^10.48 || ^11.34"
+    },
+    "require-dev": {
+        "drupal/schemata": "^1.0",
+        "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1"
     }
 }
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..26fd1d4fb4edf88d212f136fbc26f21542089b81
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,27 @@
+# AI - ECA
+
+Artificial Intelligence integration for Event-Condition-Action module, combining the unified framework
+and the power of Drupal to perform various AI-related operations.
+
+## Dependencies
+
+- Drupal ^10.3 || ^11
+- [AI module](https://drupal.org/project/ai)
+- [ECA module](https://drupal.org/project/eca)
+
+## Getting started
+
+1. Enable the module
+2. Open an ECA model
+3. Use an operation type (`Chat`, `Moderation`, `TextToSpeech` etc.) as action
+
+## Documentation
+
+This project uses MkDocs for documentation: you can see the current
+documentation at [https://project.pages.drupalcode.org/ai/](https://project.pages.drupalcode.org/ai/).
+The documentation source files are located in the `docs/` directory, and to
+build your own local version of the documentation please follow these steps:
+
+1. Install MkDocs: `pip install mkdocs mkdocs-material`
+2. Run `mkdocs serve` in the project root
+3. Open `http://localhost:8000` in your browser
diff --git a/docs/modules/agents/index.md b/docs/modules/agents/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c949ccda12e1a30785e7d5799767ac4264c632
--- /dev/null
+++ b/docs/modules/agents/index.md
@@ -0,0 +1,36 @@
+# AI ECA - Agents
+
+WARNING: this agent has the capability of creating and editing ECA models, without "realizing" that the result may be
+destructive. Meaning that, if you ask it to create a model that removes all nodes when a user logs in, nothing is
+holding it back.
+USE AT YOUR OWN RISK!
+
+## Scope
+
+- Answer questions about existing models
+- Answer questions about specific components, like events, conditions or actions
+- Create new models
+- Edit existing models
+
+## Usage
+
+- Go to `/admin/config/workflow/eca`
+- Click on `Ask AI` and write down your question
+  - The same button is available on the edit-screen of a model
+
+## Roadmap
+
+This has no specific order or priority, just things that I can think of right now
+
+- Make the prompt more "deterministic": there's an issue where the LLM edits an existing model and doesn't follow the provided JSON schema.
+- Refactor the custom Typed Data model once config validation is in core
+  - https://www.drupal.org/project/eca/issues/3446331
+- Create a separate sub-agent that can interpret an image and provide a prompt for the main one
+  - See Webform Agent
+
+## Resources
+
+- https://webthesis.biblio.polito.it/31175/1/tesi.pdf
+- https://github.com/RobJenks/gpt-codegen
+- https://github.com/jtlicardo/bpmn-assistant
+- https://ceur-ws.org/Vol-3758/paper-15.pdf
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000000000000000000000000000000000000..cb5d184992c5be6db5662cfac230e5c14efe5a43
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,3 @@
+# Usage
+
+@todo describe which operation type is exposed as an action
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0bc96944e1c67cb6937dd3c73a272ea2fd5fdfcf
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,10 @@
+site_name: Documentation for AI - ECA module for Drupal
+theme:
+  name: material
+nav:
+  - Home: index.md
+  - Usage: usage.md
+  - Modules:
+    - AI ECA Agents: modules/agents/index.md
+docs_dir: docs
+site_dir: site
diff --git a/modules/agents/ai_eca_agents.info.yml b/modules/agents/ai_eca_agents.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d0382da4a02f81f1e0ff565c3e6d51dbe626bf8b
--- /dev/null
+++ b/modules/agents/ai_eca_agents.info.yml
@@ -0,0 +1,11 @@
+name: 'AI ECA - Agents'
+type: module
+description: 'Enable an LLM to answer questions about models and ECA-components.'
+core_version_requirement: ^10.3 || ^11
+package: AI
+dependencies:
+  - ai_eca:ai_eca
+  - ai_agents:ai_agents
+  - eca:eca_ui
+  - schemata:schemata_json_schema
+  - token:token
diff --git a/modules/agents/ai_eca_agents.links.action.yml b/modules/agents/ai_eca_agents.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..311fb777b43cb4d586e36146b541449c30ee5380
--- /dev/null
+++ b/modules/agents/ai_eca_agents.links.action.yml
@@ -0,0 +1,6 @@
+ai_eca_agents.ask_ai:
+  route_name: ai_eca_agents.ask_ai
+  title: 'Ask AI'
+  class: '\Drupal\ai_eca_agents\Plugin\Menu\LocalAction\AiEcaAgentsDialogLocalAction'
+  appears_on:
+    - entity.eca.collection
diff --git a/modules/agents/ai_eca_agents.module b/modules/agents/ai_eca_agents.module
new file mode 100644
index 0000000000000000000000000000000000000000..5071496b1c3184478ff48403a4603ac577c59981
--- /dev/null
+++ b/modules/agents/ai_eca_agents.module
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * AI ECA Agents module file.
+ */
+
+use Drupal\ai_eca_agents\Hook\FormHooks;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+
+/**
+ * Implements hook_FORM_ID_alter.
+ */
+#[LegacyHook]
+function ai_eca_agents_form_bpmn_io_modeller_alter(&$form, FormStateInterface $form_state): void {
+  Drupal::service(FormHooks::class)->formModellerAlter($form, $form_state);
+}
diff --git a/modules/agents/ai_eca_agents.routing.yml b/modules/agents/ai_eca_agents.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d9e58a4c588cc6ba089a0c448635159fd763bc3c
--- /dev/null
+++ b/modules/agents/ai_eca_agents.routing.yml
@@ -0,0 +1,9 @@
+ai_eca_agents.ask_ai:
+  path: '/api/ai-eca-agents/ask-ai'
+  defaults:
+    _title: 'Ask AI'
+    _form: '\Drupal\ai_eca_agents\Form\AskAiForm'
+  options:
+    _admin_route: true
+  requirements:
+    _permission: 'administer eca'
diff --git a/modules/agents/ai_eca_agents.services.yml b/modules/agents/ai_eca_agents.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..feaf93f412bbf8f2db2efba16dc6c91a674315ec
--- /dev/null
+++ b/modules/agents/ai_eca_agents.services.yml
@@ -0,0 +1,35 @@
+services:
+  ai_eca_agents.services.data_provider:
+    class: Drupal\ai_eca_agents\Services\DataProvider\DataProvider
+    arguments:
+      - '@eca.service.modeller'
+      - '@eca.service.condition'
+      - '@eca.service.action'
+      - '@entity_type.manager'
+      - '@ai_eca_agents.services.model_mapper'
+      - '@serializer'
+      - '@token.tree_builder'
+
+  ai_eca_agents.services.eca_repository:
+    class: Drupal\ai_eca_agents\Services\EcaRepository\EcaRepository
+    arguments:
+      - '@entity_type.manager'
+      - '@ai_eca_agents.services.model_mapper'
+      - '@typed_data_manager'
+
+  ai_eca_agents.services.model_mapper:
+    class: Drupal\ai_eca_agents\Services\ModelMapper\ModelMapper
+    arguments:
+      - '@typed_data_manager'
+      - '@serializer'
+
+  # Typed data definitions in general can take many forms. This handles final items.
+  serializer.normalizer.data_definition.schema_json_ai_eca_agents.json:
+    class: Drupal\ai_eca_agents\Normalizer\json\DataDefinitionNormalizer
+    decorates: serializer.normalizer.data_definition.schema_json.json
+    arguments:
+      - '@serializer.normalizer.data_definition.schema_json_ai_eca_agents.json.inner'
+
+  Drupal\ai_eca_agents\Hook\FormHooks:
+    class: Drupal\ai_eca_agents\Hook\FormHooks
+    autowire: true
diff --git a/modules/agents/drush.services.yml b/modules/agents/drush.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..572d0cd9f23982b282478d49edae446800d844f1
--- /dev/null
+++ b/modules/agents/drush.services.yml
@@ -0,0 +1,7 @@
+services:
+  ai_eca_agents.debug_data_provider:
+    class: Drupal\ai_eca_agents\Command\DebugDataProviderCommand
+    arguments:
+      - '@ai_eca_agents.services.data_provider'
+    tags:
+      - { name: console.command }
diff --git a/modules/agents/prompts/eca/answerQuestion.yml b/modules/agents/prompts/eca/answerQuestion.yml
new file mode 100644
index 0000000000000000000000000000000000000000..451f0e7d388f9a6093eeb904cc102890ebe46c52
--- /dev/null
+++ b/modules/agents/prompts/eca/answerQuestion.yml
@@ -0,0 +1,25 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: false
+name: Answer question
+description: This sub-agent is capable of answering questions about existing models, events, conditions or actions that are available in the website.
+prompt:
+  introduction: |
+    You are a Drupal developer that can answer questions about a possible model of the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+
+    Based on a previous prompt, you will given information about a model or technical details regarding the events, conditions or actions. The input might also be a combination of those things, try to specify your answer based on those.
+
+    Be helpful, elaborate and answer in the best way you can. Try to re-use as much information about the components and which technical information they expose. This can be the tokens that they expose or the module that implements it.
+    If you do not have enough information to answer the question, please let the user know.
+
+    You can answer with multiple objects if needed.
+  possible_actions:
+    info: The answer to the question or the feedback that you do not have enough information to answer that.
+  formats:
+    - action: Action ID from the list
+      answer: The answer to the question
+  one_shot_learning_examples:
+    - action: info
+      answer: The model 'User login' can redirect a user to the correct page.
+    - action: info
+      answer: There are a couple of actions that can be used to do something with an entity, like unpublishing, updating the URL alias etc.
diff --git a/modules/agents/prompts/eca/buildModel.yml b/modules/agents/prompts/eca/buildModel.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a2fde9d9af7492b454eafb771f561034f3ba099e
--- /dev/null
+++ b/modules/agents/prompts/eca/buildModel.yml
@@ -0,0 +1,46 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: false
+name: Build model
+description: This sub-agent is capable of creating ECA models.
+validation:
+  - [eca_validation]
+retries: 2
+prompt:
+  introduction: |
+    You are a business process modelling expert, following the BPMN 2.0 standard. The prompts will contain a textual
+    description of a business process or updates that you need to execute.
+    Generate a JSON model for the process, complying to the provided JSON Schema. YOU CAN NOT DEVIATE FROM THAT.
+
+    Analyze and identify key elements:
+    1. Start events, there should be at least one;
+    2. Tasks and their sequence;
+    3. Gateways (xor) and an array of ”branches” containing tasks. There is a condition for the decision point and each
+    branch has a condition label;
+    4. Loops: involve repeating tasks until a specific condition is met.
+
+    Other requirements:
+    * Nested structure: the schema uses nested structures within gateways to represent branching paths;
+    * When analyzing the process description, identify opportunities to model tasks as parallel whenever possible for
+    optimization (if it does not contradict the user intended sequence);
+    * Use clear names for labels and conditions: e.g., instead of "Event 1" or "Action 2", use "User login" or
+    "Create node");
+    * All elements, except gateways, must have a plugin assigned to them and optionally an array of configuration
+    parameters. You will given a list of possible plugins and their corresponding configuration structure, you can not
+    deviate from those;
+    * When given an existing model, you are allowed to modify any part of it.
+
+  possible_actions:
+    build: The generated JSON model that should be imported.
+    fail: You are unable to create or alter the model.
+  formats:
+    - action: Action ID from the list
+      model: The JSON model of the process
+      message: A small summary of the model and how you came to the provided solution or feedback why you were not able to generate a model.
+  one_shot_learning_examples:
+    - action: build
+      model: |
+        {"model_id":"create_article","label":"Create Article on Page Publish","events":[{"element_id":"event_1","plugin_id":"content_entity:insert","label":"New Page Published","configuration":{"type":"node page"},"successors":[{"element_id":"action_2"}]}],"actions":[{"element_id":"action_2","plugin_id":"eca_token_set_value","label":"Set Page Title Token","configuration":{"token_name":"page_title","token_value":"[entity:title]","use_yaml":false},"successors":[{"element_id":"action_3"}]},{"element_id":"action_3","plugin_id":"eca_token_load_user_current","label":"Load Author Info","configuration":{"token_name":"author_info"},"successors":[{"element_id":"action_4"}]},{"element_id":"action_4","plugin_id":"eca_new_entity","label":"Create New Article","configuration":{"token_name":"new_article","type":"node article","langcode":"en","label":"New Article: [page_title]","published":true,"owner":"[author_info:uid]"},"successors":[{"element_id":"action_5"}]},{"element_id":"action_5","plugin_id":"eca_set_field_value","label":"Set Article Body","configuration":{"field_name":"body.value","field_value":"New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.","method":"set:force_clear","strip_tags":false,"trim":true,"save_entity":true},"successors":[{"element_id":"action_6"}]},{"element_id":"action_6","plugin_id":"eca_save_entity","label":"Save Article","successors":[]}]}
+      message: The model "Create Article on Page Publish" creates an article about a new published page, it contains the label and the author of that new page.
+    - action: fail
+      message: I was unable to analyze the description.
diff --git a/modules/agents/prompts/eca/determineTask.yml b/modules/agents/prompts/eca/determineTask.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d41e8555d51ebca9353148bcb8971066e58234b3
--- /dev/null
+++ b/modules/agents/prompts/eca/determineTask.yml
@@ -0,0 +1,57 @@
+preferred_model: gpt-4o
+preferred_llm: openai
+is_triage: true
+name: Determine task
+Description: This agent is the initial agent that determines if the user is trying to manipulate ECA models, wants information or ask questions about models or plugins.
+prompt:
+  introduction: |
+    You are a Drupal developer that can create or edit configuration for the Event-Condition-Action module. You are also capable of answering questions about the different events, conditions or actions that are available in the website.
+
+    Based on the following context of a task description and comments, you should figure out if they are trying to create a new model, edit an existing one or just asking a question that requires no action. Any question that was already answered, will not be marked as a question.
+
+    You will be given a list of existing models and any events, condition- or action-plugins that are present in the website.
+
+    If the action is "create", "edit" or "info", include the IDs of the existing components you think are required to execute the action as "component_ids" in your answer. There can be multiple but do not generate new IDs.
+    If the action is "edit", include the ID of the model as "model_id" in your answer. There can only be one model ID and do not generate a new model ID.
+    If the action is "info" and you believe that an existing model might be the subject of the answer, provide the model ID as "model_id". There can only be one model ID and do not generate a new model ID.
+
+    If you have suggestions on how the determined task type (eg. "create" or "info") should be executed, you can provide that feedback as "feedback" in your answer. Be precise.
+    If you can't find the answer to the question, you can ask for more information or say that you can't find the answer.
+
+    Only give back one long answer.
+  possible_actions:
+    create: They are trying to create an Event-Condition-Action model.
+    edit: They are trying to edit an existing Event-Condition-Action model.
+    info: They want information about an existing model or one or more existing events, conditions or actions, without any further action.
+    fail: It failed due to missing information or being ambivalent.
+  formats:
+    - action: Action ID from the list
+      model_id: The ID of the existing model when the action is "edit" or "info"
+      component_ids: A list of IDs of components when the action is "create", "edit" or "info"
+      feedback: An optional message
+  one_shot_learning_examples:
+    - action: create
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: edit
+      model_id: user_login
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: info
+      model_id: user_login
+    - action: info
+      component_ids:
+        - 'user:login'
+        - 'content_entity:insert'
+        - 'eca_current_user_role'
+        - 'action_goto_action'
+    - action: fail
+      feedback: The model "User login" does not exist
+    - action: fail
+      feedback: There is no condition plugin about checking the age of the user.
diff --git a/modules/agents/src/Command/DebugDataProviderCommand.php b/modules/agents/src/Command/DebugDataProviderCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..105af68d52611d91d29afe7b0fe6460ba010e514
--- /dev/null
+++ b/modules/agents/src/Command/DebugDataProviderCommand.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ai_eca_agents\Command;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * A console command to debug the data provider.
+ */
+#[AsCommand(
+  name: 'ai-agents-eca:debug:data',
+  description: 'Debug the data provider',
+)]
+final class DebugDataProviderCommand extends Command {
+
+  /**
+   * Constructs an DebugCommand object.
+   */
+  public function __construct(
+    protected readonly DataProviderInterface $dataProvider,
+  ) {
+    parent::__construct();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure(): void {
+    $this
+      ->addOption('filter', NULL, InputOption::VALUE_OPTIONAL, 'The filter to apply to the provider. Options are "components" or "models"')
+      ->addOption('vm', NULL, InputOption::VALUE_OPTIONAL, 'The view mode to use. Options are teaser or full.', DataViewModeEnum::Teaser->value)
+      ->addUsage('ai-agents-eca:debug:data --vm=full')
+      ->setHelp('Debug the data provider');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output): int {
+    $this->dataProvider->setViewMode(DataViewModeEnum::from($input->getOption('vm')));
+    $response = [
+      'components' => $this->dataProvider->getComponents(),
+      'models' => $this->dataProvider->getModels(),
+    ];
+    if (!empty($input->getOption('filter'))) {
+      $filter = $input->getOption('filter');
+      $response = array_filter($response, function (string $key) use ($filter) {
+        return $key === $filter;
+      }, ARRAY_FILTER_USE_KEY);
+    }
+
+    $response = Json::decode(Json::encode($response));
+
+    $output->writeln(print_r($response, TRUE));
+
+    return self::SUCCESS;
+  }
+
+}
diff --git a/modules/agents/src/EcaElementType.php b/modules/agents/src/EcaElementType.php
new file mode 100644
index 0000000000000000000000000000000000000000..9582e7ebec4af74f0da188394618862b3d27550a
--- /dev/null
+++ b/modules/agents/src/EcaElementType.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+/**
+ * Enum of the element types.
+ */
+enum EcaElementType: string {
+
+  case Event = 'event';
+  case Condition = 'condition';
+  case Action = 'action';
+  case Gateway = 'gateway';
+
+  /**
+   * Get the plural form of a type.
+   *
+   * @return string
+   *   Returns the plural form of a type.
+   */
+  public function getPlural(): string {
+    return match ($this) {
+      self::Event => 'events',
+      self::Condition => 'conditions',
+      self::Action => 'actions',
+      self::Gateway => 'gateways',
+    };
+  }
+
+  /**
+   * Returns a list of types, keyed by their plural form.
+   *
+   * @return \Drupal\ai_eca_agents\EcaElementType[]
+   *   The list of types, keyed by their plural form.
+   */
+  public static function getPluralMap(): array {
+    return [
+      self::Event->getPlural() => self::Event,
+      self::Condition->getPlural() => self::Condition,
+      self::Action->getPlural() => self::Action,
+      self::Gateway->getPlural() => self::Gateway,
+    ];
+  }
+
+}
diff --git a/modules/agents/src/EntityViolationException.php b/modules/agents/src/EntityViolationException.php
new file mode 100644
index 0000000000000000000000000000000000000000..d498b2d99ff53237d2789a956a1ec09b5f0af38a
--- /dev/null
+++ b/modules/agents/src/EntityViolationException.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+use Drupal\Core\Config\ConfigException;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * An exception thrown when violations against the schema are captured.
+ */
+class EntityViolationException extends ConfigException {
+
+  /**
+   * The constraint violations associated with this exception.
+   *
+   * @var \Symfony\Component\Validator\ConstraintViolationListInterface|null
+   */
+  protected ?ConstraintViolationListInterface $violations;
+
+  /**
+   * EntityViolationException constructor.
+   *
+   * @param string $message
+   *   The Exception message to throw.
+   * @param int $code
+   *   The Exception code.
+   * @param \Throwable|null $previous
+   *   The previous throwable used for the exception chaining.
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface|null $violations
+   *   The constraint violations associated with this exception.
+   */
+  public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = NULL, ?ConstraintViolationListInterface $violations = NULL) {
+    $this->violations = $violations;
+
+    if (empty($message)) {
+      $message = 'Validation of the entity failed.';
+    }
+    if (!empty($violations)) {
+      $message = sprintf('%s Violations: %s.', $message, (string) $violations);
+    }
+
+    parent::__construct($message, $code, $previous);
+  }
+
+  /**
+   * Gets the constraint violations associated with this exception.
+   *
+   * @return \Symfony\Component\Validator\ConstraintViolationListInterface|null
+   *   The constraint violations.
+   */
+  public function getViolations(): ?ConstraintViolationListInterface {
+    return $this->violations;
+  }
+
+}
diff --git a/modules/agents/src/Form/AskAiForm.php b/modules/agents/src/Form/AskAiForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..ce4e8655c762449b1b53d81363488068dfb0eb77
--- /dev/null
+++ b/modules/agents/src/Form/AskAiForm.php
@@ -0,0 +1,221 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Form;
+
+use Drupal\Core\Batch\BatchBuilder;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
+use Drupal\ai_agents\Task\Task;
+use Drupal\ai_eca_agents\Plugin\AiAgent\Eca;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A form to propose a question to AI.
+ */
+class AskAiForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId(): string {
+    return 'ai_eca_agents_ask_ai';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state): array {
+    $form['question'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Propose your question'),
+      '#required' => TRUE,
+    ];
+
+    // Determine the destination for when the batch process is finished.
+    $destination = $this->getRequest()->query->get('destination', Url::fromRoute('entity.eca.collection')->toString());
+    $form['destination'] = [
+      '#type' => 'value',
+      '#value' => $destination,
+    ];
+
+    // Determine if an existing model is the subject of the prompt.
+    $modelId = $this->getRequest()->query->get('model-id');
+    $form['model_id'] = [
+      '#type' => 'value',
+      '#value' => $modelId,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Ask'),
+      '#button_type' => 'primary',
+    ];
+    $form['#theme'] = 'confirm_form';
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state): void {
+    $batch = new BatchBuilder();
+    $batch->setTitle($this->t('Solving question.'));
+    $batch->setInitMessage($this->t('Asking AI which task to perform...'));
+    $batch->setErrorMessage($this->t('An error occurred during processing.'));
+    $batch->setFinishCallback([self::class, 'batchFinished']);
+
+    $batch->addOperation([self::class, 'initProcess'], [$form_state->getValue('destination')]);
+    $batch->addOperation([self::class, 'determineTask'], [
+      $form_state->getValue('question'),
+      $form_state->getValue('model_id'),
+    ]);
+    $batch->addOperation([self::class, 'executeTask']);
+
+    batch_set($batch->toArray());
+  }
+
+  /**
+   * Batch operation for initializing the process.
+   *
+   * @param string $destination
+   *   The destination for when the process is finished.
+   * @param array $context
+   *   The context.
+   */
+  public static function initProcess(string $destination, array &$context): void {
+    $context['message'] = t('Initializing...');
+
+    $context['results']['destination'] = $destination;
+  }
+
+  /**
+   * Batch operation for determining the task to execute.
+   *
+   * @param string $question
+   *   The question of the user.
+   * @param string|null $modelId
+   *   The ECA model ID.
+   * @param array $context
+   *   The context.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public static function determineTask(string $question, ?string $modelId, array &$context): void {
+    $context['message'] = t('Task determined. Executing...');
+
+    $context['sandbox']['question'] = $question;
+    $context['sandbox']['model_id'] = $modelId;
+    $agent = self::initAgent($context);
+
+    // Let the agent decide how it can answer the question.
+    $solvability = $agent->determineSolvability();
+
+    if ($solvability === AiAgentInterface::JOB_NOT_SOLVABLE) {
+      $context['results']['error'] = t('The AI agent could not solve the task.');
+
+      return;
+    }
+
+    // Redirect to the convert-endpoint of the BPMN.io-module.
+    $bpmnIoIsAvailable = \Drupal::moduleHandler()->moduleExists('bpmn_io');
+    if ($solvability === AiAgentInterface::JOB_SOLVABLE && $bpmnIoIsAvailable && !empty($modelId)) {
+      $context['results']['destination'] = Url::fromRoute('bpmn_io.convert', [
+        'eca' => $modelId,
+      ])->toString();
+    }
+
+    $context['results']['dto'] = $agent->getDto();
+  }
+
+  /**
+   * Batch operation for executing the task.
+   *
+   * @param array $context
+   *   The context.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public static function executeTask(array &$context): void {
+    $context['message'] = t('Task executed. Gathering feedback...');
+
+    $agent = self::initAgent($context);
+
+    // Solve the question.
+    $response = $agent->solve();
+    $context['results']['response'] = $response;
+  }
+
+  /**
+   * Callback for when the batch is finished.
+   *
+   * @param bool $success
+   *   A boolean indicating a successful execution.
+   * @param array $results
+   *   The collection of results.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   Returns a http-response.
+   */
+  public static function batchFinished(bool $success, array $results): Response {
+    if (!empty($results['response'])) {
+      \Drupal::messenger()->addStatus($results['response']);
+    }
+
+    return new RedirectResponse($results['destination']);
+  }
+
+  /**
+   * Initialize the AI agent.
+   *
+   * @param array $context
+   *   The context.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\AiAgent\Eca
+   *   Returns the AI agent.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  protected static function initAgent(array $context): Eca {
+    // Initiate the agent.
+    /** @var \Drupal\ai_agents\PluginInterfaces\AiAgentInterface $agent */
+    $agent = \Drupal::service('plugin.manager.ai_agents')
+      ->createInstance('eca');
+    assert($agent instanceof Eca);
+
+    $dto = [];
+    if (!empty($context['sandbox']['model_id'])) {
+      $dto['model_id'] = $context['sandbox']['model_id'];
+    }
+    if (!empty($context['results']['dto']) && is_array($context['results']['dto'])) {
+      $dto = array_merge($dto, $context['results']['dto']);
+      $dto['setup_agent'] = TRUE;
+    }
+    if (!empty($dto)) {
+      $agent->setDto($dto);
+    }
+
+    // Prepare the task.
+    $question = $context['sandbox']['question'] ?? NULL;
+    $question = $dto['task_description'] ?? $question;
+    $task = new Task($question);
+    $agent->setTask($task);
+
+    // Set the provider.
+    /** @var \Drupal\ai\AiProviderPluginManager $providerManager */
+    $providerManager = \Drupal::service('ai.provider');
+    $provider = $providerManager->getDefaultProviderForOperationType('chat');
+    $agent->setAiProvider($providerManager->createInstance($provider['provider_id']));
+    $agent->setModelName($provider['model_id']);
+    $agent->setAiConfiguration([]);
+
+    return $agent;
+  }
+
+}
diff --git a/modules/agents/src/Hook/FormHooks.php b/modules/agents/src/Hook/FormHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0494334ee9d7c9b41fd048d5ed4207c177b35b06
--- /dev/null
+++ b/modules/agents/src/Hook/FormHooks.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Hook;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Provides hook implementations for form alterations.
+ */
+class FormHooks {
+
+  use StringTranslationTrait;
+
+  /**
+   * Constructs a FormHooks instance.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
+   *   The route match.
+   */
+  public function __construct(
+    protected RouteMatchInterface $routeMatch,
+  ) {
+  }
+
+  /**
+   * Alters the ECA Modeller form.
+   *
+   * @param array $form
+   *   The form.
+   * @param \Drupal\Core\Form\FormStateInterface $formState
+   *   The form state.
+   */
+  #[Hook('form_bpmn_io_modeller_alter')]
+  public function formModellerAlter(array &$form, FormStateInterface $formState): void {
+    // Take the options from the export-link.
+    /** @var \Drupal\Core\Url $exportLink */
+    $exportLink = $form['actions']['export_archive']['#url'];
+    $options = $exportLink->getOptions();
+
+    $eca = $this->routeMatch->getParameter('eca');
+    if ($eca instanceof Eca) {
+      $eca = $eca->id();
+    }
+    $options['query']['model-id'] = $eca;
+
+    $form['actions']['ask_ai'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Ask AI'),
+      '#url' => Url::fromRoute('ai_eca_agents.ask_ai', [], $options),
+      '#attributes' => [
+        'class' => ['button', 'ai-eca-ask-ai', 'use-ajax'],
+        'data-dialog-type' => 'modal',
+        'data-dialog-options' => Json::encode([
+          'width' => 1000,
+          'dialogClass' => 'ui-dialog-off-canvas',
+        ]),
+      ],
+      '#attached' => [
+        'library' => [
+          'core/drupal.dialog.ajax',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/modules/agents/src/MissingEventException.php b/modules/agents/src/MissingEventException.php
new file mode 100644
index 0000000000000000000000000000000000000000..401a07170106b1be5f311aebeabb71934c82eec0
--- /dev/null
+++ b/modules/agents/src/MissingEventException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\ai_eca_agents;
+
+use Drupal\Core\Config\ConfigException;
+
+/**
+ * An exception thrown when no events are present.
+ */
+class MissingEventException extends ConfigException {
+
+}
diff --git a/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..72aef2ff835bee916cc071268c78e26609a90d06
--- /dev/null
+++ b/modules/agents/src/Normalizer/json/DataDefinitionNormalizer.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Normalizer\json;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\schemata_json_schema\Normalizer\json\JsonNormalizerBase;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Normalizer for DataDefinitionInterface instances.
+ *
+ * DataDefinitionInterface is the ultimate parent to all data definitions. This
+ * service must always be low priority for data definitions, otherwise the
+ * simpler normalization process it supports will take precedence over all the
+ * complexities most entity properties contain before reaching this level.
+ *
+ * DataDefinitionNormalizer produces scalar value definitions.
+ *
+ * Unlike the other Normalizer services in the JSON Schema module, this one is
+ * used by the hal_schemata normalizer. It is unlikely divergent requirements
+ * will develop.
+ *
+ * All the TypedData normalizers extend from this class.
+ */
+class DataDefinitionNormalizer extends JsonNormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected string $supportedInterfaceOrClass = DataDefinitionInterface::class;
+
+  /**
+   * The inner normalizer.
+   *
+   * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface
+   */
+  protected NormalizerInterface $inner;
+
+  /**
+   * Constructs a DataDefinitionNormalizer object.
+   *
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The decorated normalizer.
+   */
+  public function __construct(NormalizerInterface $normalizer) {
+    $this->inner = $normalizer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($entity, $format = NULL, array $context = []): array|bool|string|int|float|null|\ArrayObject {
+    $normalized = $this->inner->normalize($entity, $format, $context);
+
+    if (
+      empty($entity->getSetting('allowed_values_function'))
+      && !empty($entity->getSetting('allowed_values'))
+    ) {
+      $normalized['properties'][$context['name']]['enum'] = array_keys($entity->getSetting('allowed_values'));
+    }
+
+    return $normalized;
+  }
+
+}
diff --git a/modules/agents/src/Plugin/AiAgent/Eca.php b/modules/agents/src/Plugin/AiAgent/Eca.php
new file mode 100644
index 0000000000000000000000000000000000000000..a0fe9b2b8794362adf5d91f6d5a96f42a75579c9
--- /dev/null
+++ b/modules/agents/src/Plugin/AiAgent/Eca.php
@@ -0,0 +1,381 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\AiAgent;
+
+use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\Component\Render\MarkupInterface;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\ai_agents\Attribute\AiAgent;
+use Drupal\ai_agents\PluginBase\AiAgentBase;
+use Drupal\ai_agents\PluginInterfaces\AiAgentInterface;
+use Drupal\ai_agents\Task\TaskInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface;
+use Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum;
+use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
+use Drupal\eca\Entity\Eca as EcaEntity;
+use Illuminate\Support\Arr;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * The ECA agent.
+ */
+#[AiAgent(
+  id: 'eca',
+  label: new TranslatableMarkup('ECA Agent'),
+)]
+class Eca extends AiAgentBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The DTO.
+   *
+   * @var array
+   */
+  protected array $dto;
+
+  /**
+   * The ECA entity.
+   *
+   * @var \Drupal\eca\Entity\Eca|null
+   */
+  protected ?EcaEntity $model = NULL;
+
+  /**
+   * The ECA data provider.
+   *
+   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
+   */
+  protected DataProviderInterface $dataProvider;
+
+  /**
+   * The ECA helper.
+   *
+   * @var \Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface
+   */
+  protected EcaRepositoryInterface $ecaRepository;
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected SerializerInterface $serializer;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+    $instance->dataProvider = $container->get('ai_eca_agents.services.data_provider');
+    $instance->ecaRepository = $container->get('ai_eca_agents.services.eca_repository');
+    $instance->serializer = $container->get('serializer');
+    $instance->dto = [
+      'task_description' => '',
+      'feedback' => '',
+      'questions' => [],
+      'data' => [],
+      'logs' => [],
+    ];
+
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getId(): string {
+    return 'eca';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function agentsNames(): array {
+    return ['Event-Condition-Action agent'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAvailable(): bool {
+    return $this->agentHelper->isModuleEnabled('eca');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRetries(): int {
+    return 2;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAccess() {
+    if (!$this->currentUser->hasPermission('administer eca')) {
+      return AccessResult::forbidden();
+    }
+
+    return parent::hasAccess();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function agentsCapabilities(): array {
+    return [
+      'eca' => [
+        'name' => 'Event-Condition-Action Agent',
+        'description' => 'This is agent is capable of adding, editing or informing about Event-Condition-Action models on a Drupal website. Note that it does not add events, conditions or actions as those require a specific implementation via code.',
+        'inputs' => [
+          'free_text' => [
+            'name' => 'Prompt',
+            'type' => 'string',
+            'description' => 'The prompt to create, edit or ask questions about Event-Condition-Actions models.',
+            'default_value' => '',
+          ],
+        ],
+        'outputs' => [
+          'answers' => [
+            'description' => 'The answers to the questions asked about the Event-Condition-Action model.',
+            'type' => 'string',
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function determineSolvability(): int {
+    parent::determineSolvability();
+    $type = $this->determineTypeOfTask();
+
+    return match ($type) {
+      'create', 'edit' => AiAgentInterface::JOB_SOLVABLE,
+      'info' => AiAgentInterface::JOB_SHOULD_ANSWER_QUESTION,
+      'fail' => AiAgentInterface::JOB_NEEDS_ANSWERS,
+      default => AiAgentInterface::JOB_NOT_SOLVABLE,
+    };
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function solve(): array|string {
+    if (isset($this->dto['setup_agent']) && $this->dto['setup_agent'] === TRUE) {
+      parent::determineSolvability();
+    }
+
+    switch (Arr::get($this->dto, 'data.0.action')) {
+      case 'create':
+      case 'edit':
+        $this->buildModel();
+        break;
+
+      case 'info':
+        $log = $this->answerQuestion();
+        $this->dto['logs'][] = $log;
+        break;
+    }
+
+    return Arr::join($this->dto['logs'], "\n");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function answerQuestion(): string|TranslatableMarkup {
+    if (!$this->currentUser->hasPermission('administer eca')) {
+      return $this->t('You do not have permission to do this.');
+    }
+
+    $this->dataProvider->setViewMode(DataViewModeEnum::Full);
+
+    $context = [];
+    if (!empty($this->model)) {
+      $context['The information of the model, in JSON format'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels([$this->model->id()])));
+    }
+    elseif (!empty($this->data[0]['component_ids'])) {
+      $context['The details about the components'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents($this->data[0]['component_ids'])));
+    }
+
+    if (empty($context)) {
+      return $this->t('Sorry, I could not answer your question without anymore context.');
+    }
+
+    // Perform the prompt.
+    $data = $this->agentHelper->runSubAgent('answerQuestion', $context);
+
+    if (isset($data[0]['answer'])) {
+      $answer = array_map(function ($dataPoint) {
+        return $dataPoint['answer'];
+      }, $data);
+
+      return implode("\n", $answer);
+    }
+
+    return $this->t('Sorry, I got no answers for you.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHelp(): MarkupInterface {
+    return $this->t('This agent can figure out event-condition-action models of a file. Just upload and ask.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function askQuestion(): array {
+    return (array) Arr::get($this->dto, 'questions');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function approveSolution(): void {
+    $this->dto = Arr::set($this->dto, 'data.0.action', 'create');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTask(TaskInterface $task): void {
+    parent::setTask($task);
+
+    $this->dto['task_description'] = $task->getDescription();
+  }
+
+  /**
+   * Get the data transfer object.
+   *
+   * @return array
+   *   Returns the data transfer object.
+   */
+  public function getDto(): array {
+    return $this->dto;
+  }
+
+  /**
+   * Set the data transfer object.
+   *
+   * @param array $dto
+   *   The data transfer object.
+   */
+  public function setDto(array $dto): void {
+    $this->dto = $dto;
+
+    if (!empty($this->dto['model_id'])) {
+      $this->model = $this->ecaRepository->get($this->dto['model_id']);
+    }
+  }
+
+  /**
+   * Determine the type of task.
+   *
+   * @return string
+   *   Returns the type of task.
+   *
+   * @throws \Exception
+   */
+  protected function determineTypeOfTask(): string {
+    $context = $this->getFullContextOfTask($this->task);
+    $userContext = [
+      'A summary of the existing models' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels())),
+      'The list of available events, conditions and actions, in JSON format' => sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents())),
+    ];
+    if (!empty($this->model)) {
+      $context .= "A model already exists, so creation is not possible.";
+      $userContext['A summary of the existing models'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getModels([$this->model->id()])));
+    }
+    $userContext['Task description and if available comments description'] = $context;
+
+    // Prepare and run the prompt by fetching all the relevant info.
+    $data = $this->agentHelper->runSubAgent('determineTask', $userContext);
+
+    // Quit early if the returned response isn't what we expected.
+    if (empty($data[0]['action'])) {
+      $this->dto['questions'][] = 'Sorry, we could not understand what you wanted to do, please try again.';
+
+      return 'fail';
+    }
+
+    if (in_array($data[0]['action'], [
+      'create',
+      'edit',
+      'info',
+    ])) {
+      if (!empty($data[0]['model_id'])) {
+        $this->dto['model_id'] = $data[0]['model_id'];
+        $this->model = $this->ecaRepository->get($data[0]['model_id']);
+      }
+
+      if (!empty($data[0]['feedback'])) {
+        $this->dto['feedback'] = $data[0]['feedback'];
+      }
+
+      $this->dto['data'] = $data;
+
+      return $data[0]['action'];
+    }
+
+    throw new \Exception('Invalid action determined for ECA');
+  }
+
+  /**
+   * Create a configuration item for ECA.
+   */
+  protected function buildModel(): void {
+    $this->dataProvider->setViewMode(DataViewModeEnum::Full);
+
+    // Prepare the prompt.
+    $context = [];
+
+    // The schema of the ECA-config that the LLM should follow.
+    $definition = EcaModelDefinition::create();
+    $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
+    $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
+    $context['JSON Schema of the model'] = sprintf("```json\n%s\n```", $schema);
+
+    // The model that should be edited.
+    if (!empty($this->model)) {
+      $models = $this->dataProvider->getModels([$this->model->id()]);
+      $context['The model to edit'] = sprintf("```json\n%s\n```", Json::encode(reset($models)));
+    }
+
+    // Components or plugins that the LLM should use.
+    if (Arr::has($this->dto, 'data.0.component_ids')) {
+      $componentIds = Arr::get($this->dto, 'data.0.component_ids', []);
+      $context['The details about the components'] = sprintf("```json\n%s\n```", Json::encode($this->dataProvider->getComponents($componentIds)));
+    }
+
+    // Optional feedback that the previous prompt provided.
+    if (Arr::has($this->dto, 'data.0.feedback')) {
+      $context['Guidelines'] = Arr::get($this->dto, 'data.0.feedback');
+    }
+
+    // Execute it.
+    $data = $this->agentHelper->runSubAgent('buildModel', $context);
+
+    $message = Arr::get($data, '0.message', 'Could not create ECA config.');
+    if (
+      Arr::get($data, '0.action', 'fail') !== 'build'
+      || !Arr::has($data, '0.model')
+    ) {
+      throw new \Exception($message);
+    }
+
+    $eca = $this->ecaRepository->build(Json::decode(Arr::get($data, '0.model')), TRUE, $this->model?->id());
+
+    $this->dto['logs'][] = $this->model ? $this->t('Model "@model" was altered.', ['@model' => $this->model->label()]) : $this->t('A new model was created: "@model".', ['@model' => $eca->label()]);
+    $this->dto['logs'][] = $message;
+  }
+
+}
diff --git a/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b25949a4dd9011729409bb5a92427309c155138
--- /dev/null
+++ b/modules/agents/src/Plugin/AiAgentValidation/EcaValidation.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\AiAgentValidation;
+
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\ai\OperationType\Chat\ChatMessage;
+use Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface;
+use Drupal\ai_agents\Attribute\AiAgentValidation;
+use Drupal\ai_agents\Exception\AgentRetryableValidationException;
+use Drupal\ai_agents\PluginBase\AiAgentValidationPluginBase;
+use Drupal\ai_agents\PluginInterfaces\AiAgentValidationInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+use Illuminate\Support\Arr;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The validation of the ECA agent.
+ */
+#[AiAgentValidation(
+  id: 'eca_validation',
+  label: new TranslatableMarkup('ECA Validation'),
+)]
+class EcaValidation extends AiAgentValidationPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The JSON-decoder of the prompt.
+   *
+   * @var \Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface
+   */
+  protected PromptJsonDecoderInterface $promptJsonDecoder;
+
+  /**
+   * The model mapper.
+   *
+   * @var \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface
+   */
+  protected ModelMapperInterface $modelMapper;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): AiAgentValidationInterface {
+    $instance = new static($configuration, $plugin_id, $plugin_definition);
+    $instance->promptJsonDecoder = $container->get('ai.prompt_json_decode');
+    $instance->modelMapper = $container->get('ai_eca_agents.services.model_mapper');
+
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValidation(mixed $data): bool {
+    $data = $this->decodeData($data);
+    if (empty($data)) {
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: could not decode from JSON.',
+        0,
+        NULL,
+        'You MUST only provide a RFC8259 compliant JSON response.'
+      );
+    }
+
+    if (!Arr::has($data, '0.message')) {
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: feedback was not provided.',
+        0,
+        NULL,
+        'You MUST provide a summary of your action or give feedback why you failed.'
+      );
+    }
+
+    // Stop validation if the LLM failed to return a response.
+    if (Arr::get($data, '0.action', 'fail')) {
+      return TRUE;
+    }
+
+    // Validate the response against ECA-model schema.
+    try {
+      $this->modelMapper->fromPayload(Json::decode(Arr::get($data, '0.model')));
+    }
+    catch (MissingDataException $e) {
+      throw new AgentRetryableValidationException(
+        'The LLM response failed validation: the model definition was violated.',
+        0,
+        NULL,
+        sprintf('Fix the following violations: %s.', $e->getMessage())
+      );
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Decodes the source data.
+   *
+   * @param mixed $source
+   *   The source.
+   *
+   * @return array|null
+   *   Returns the decoded data or NULL.
+   */
+  protected function decodeData(mixed $source): ?array {
+    $text = NULL;
+
+    switch (TRUE) {
+      case $source instanceof ChatMessage:
+        return $this->promptJsonDecoder->decode($source);
+
+      case is_string($source):
+        $text = $source;
+        break;
+    }
+
+    return Json::decode($text ?? '');
+  }
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaGateway.php b/modules/agents/src/Plugin/DataType/EcaGateway.php
new file mode 100644
index 0000000000000000000000000000000000000000..8162a97e1e3e944070cfb9edae61e93a858817ea
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaGateway.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaGatewayDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Data type plugin of the ECA Gateway data type.
+ */
+#[DataType(
+  id: 'eca_gateway',
+  label: new TranslatableMarkup('ECA Gateway'),
+  definition_class: EcaGatewayDefinition::class,
+)]
+class EcaGateway extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaModel.php b/modules/agents/src/Plugin/DataType/EcaModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..fb622d09315d3beac4120ce9458cf6f6c5a81181
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaModel.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Data type plugin of the ECA Model data type.
+ */
+#[DataType(
+  id: 'eca_model',
+  label: new TranslatableMarkup('ECA Model'),
+  definition_class: EcaModelDefinition::class,
+)]
+class EcaModel extends Map {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName(): string {
+    return !empty($this->get('label')->getString()) ? $this->get('label')->getString() : 'the model';
+  }
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaPlugin.php b/modules/agents/src/Plugin/DataType/EcaPlugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..1cd4f57f01113ccfcd50d1c67d6e11eae742e517
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaPlugin.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaPluginDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Data type plugin of the ECA Plugin data type.
+ */
+#[DataType(
+  id: 'eca_plugin',
+  label: new TranslatableMarkup('ECA Plugin'),
+  definition_class: EcaPluginDefinition::class,
+)]
+class EcaPlugin extends Map {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName(): string {
+    try {
+      return $this->get('label')->getString();
+    }
+    catch (\InvalidArgumentException $e) {
+      // The definition of the plugin doesn't specify a label.
+      return parent::getName();
+    }
+  }
+
+}
diff --git a/modules/agents/src/Plugin/DataType/EcaSuccessor.php b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
new file mode 100644
index 0000000000000000000000000000000000000000..90a65a848ef1db4bc67de869fd89256d60ccae33
--- /dev/null
+++ b/modules/agents/src/Plugin/DataType/EcaSuccessor.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\DataType;
+
+use Drupal\ai_eca_agents\TypedData\EcaSuccessorDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Data type plugin of the ECA Successor data type.
+ */
+#[DataType(
+  id: 'eca_successor',
+  label: new TranslatableMarkup('ECA Successor'),
+  definition_class: EcaSuccessorDefinition::class
+)]
+class EcaSuccessor extends Map {
+
+}
diff --git a/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php b/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..fdcf975a2525f6cc04e1a6e99b200301b92e45d0
--- /dev/null
+++ b/modules/agents/src/Plugin/Menu/LocalAction/AiEcaAgentsDialogLocalAction.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Menu\LocalAction;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Defines a local action plugin with the needed dialog attributes.
+ */
+class AiEcaAgentsDialogLocalAction extends LocalActionDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions(RouteMatchInterface $route_match) {
+    $options = parent::getOptions($route_match);
+
+    $attributes = [
+      'class' => ['use-ajax'],
+      'data-dialog-type' => 'modal',
+      'data-dialog-options' => Json::encode([
+        'width' => 1000,
+        'dialogClass' => 'ui-dialog-off-canvas',
+      ]),
+    ];
+    $options['attributes'] = $this->pluginDefinition['attributes'] ?? [];
+    $options['attributes'] = NestedArray::mergeDeep($options['attributes'], $attributes);
+
+    return $options;
+  }
+
+}
diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..d064a38a95033915dc12a6c1f3d9d17480a48689
--- /dev/null
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraint.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Validation\Constraint;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Validation\Attribute\Constraint;
+use Symfony\Component\Validator\Constraint as SymfonyConstraint;
+
+/**
+ * Checks if all successors are valid.
+ */
+#[Constraint(
+  id: 'SuccessorsAreValid',
+  label: new TranslatableMarkup('Successor are valid')
+)]
+class SuccessorsAreValidConstraint extends SymfonyConstraint {
+
+  /**
+   * The error message if the successor is invalid.
+   *
+   * @var string
+   */
+  public string $invalidSuccessorMessage = "Invalid successor ID '@successorId' for @type '@elementId'. Must reference a gateway or action.";
+
+  /**
+   * The error message if the condition is invalid.
+   *
+   * @var string
+   */
+  public string $invalidConditionMessage = "Invalid condition '@conditionId' for successor of @type '@elementId'. Must be in the list of conditions.";
+
+  /**
+   * The error message if the event is succeeded by another event.
+   *
+   * @var string
+   */
+  public string $disallowedSuccessorEvent = "Event '@elementId' cannot be succeeded by another event.";
+
+  /**
+   * The error message if a non-event element is succeeded by an event.
+   *
+   * @var string
+   */
+  public string $disallowedSuccessor = "@type '@elementId' cannot be succeeded by an event.";
+
+}
diff --git a/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..41d8968acfbd9d53bc4a797e763e2e64dcbeb4a5
--- /dev/null
+++ b/modules/agents/src/Plugin/Validation/Constraint/SuccessorsAreValidConstraintValidator.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Plugin\Validation\Constraint;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validator of the SuccessorsAreValid-constraint.
+ */
+class SuccessorsAreValidConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint): void {
+    assert($constraint instanceof SuccessorsAreValidConstraint);
+
+    $lookup = [
+      EcaElementType::Event->getPlural() => array_flip(array_column($value['events'] ?? [], 'element_id')),
+      EcaElementType::Condition->getPlural() => array_flip(array_column($value['conditions'] ?? [], 'element_id')),
+      EcaElementType::Action->getPlural() => array_flip(array_column($value['actions'] ?? [], 'element_id')),
+      EcaElementType::Gateway->getPlural() => array_flip(array_column($value['gateways'] ?? [], 'gateway_id')),
+    ];
+
+    foreach (EcaElementType::getPluralMap() as $plural => $type) {
+      if (empty($value[$plural])) {
+        continue;
+      }
+
+      foreach ($value[$plural] as $element) {
+        $this->validateSuccessor($constraint, $element, $type, $lookup);
+      }
+    }
+  }
+
+  /**
+   * Validate the successors of an element.
+   *
+   * @param \Drupal\ai_eca_agents\Plugin\Validation\Constraint\SuccessorsAreValidConstraint $constraint
+   *   The constraint.
+   * @param array $element
+   *   The element to validate.
+   * @param \Drupal\ai_eca_agents\EcaElementType $type
+   *   The type of the element.
+   * @param array $lookup
+   *   The lookup-array containing all the referencable IDs.
+   */
+  protected function validateSuccessor(SuccessorsAreValidConstraint $constraint, array $element, EcaElementType $type, array $lookup): void {
+    if (empty($element['successors'])) {
+      return;
+    }
+
+    foreach ($element['successors'] as $successor) {
+      $successorId = $successor['element_id'] ?? $element['gateway_id'];
+      $conditionId = $successor['condition'] ?? NULL;
+
+      // Check if the successor ID is valid (must be a gateway or an action).
+      if (
+        !isset($lookup[EcaElementType::Action->getPlural()][$successorId])
+        && !isset($lookup[EcaElementType::Gateway->getPlural()][$successorId])
+      ) {
+        $this->context->addViolation($constraint->invalidSuccessorMessage, [
+          '@successorId' => $successorId,
+          '@type' => $type->name,
+          '@elementId' => $element['element_id'],
+        ]);
+      }
+
+      // Check if the condition ID is valid.
+      if ($conditionId && !isset($lookup[EcaElementType::Condition->getPlural()][$conditionId])) {
+        $this->context->addViolation($constraint->invalidConditionMessage, [
+          '@conditionId' => $conditionId,
+          '@type' => $type->name,
+          '@elementId' => $element['element_id'],
+        ]);
+      }
+
+      // Check for disallowed successor relationships.
+      if (
+        ($type === EcaElementType::Action || $type === EcaElementType::Gateway)
+        && isset($lookup[EcaElementType::Event->getPlural()][$successorId])
+      ) {
+        $elementId = $element['element_id'] ?? $element['gateway_id'];
+        $this->context->addViolation($constraint->disallowedSuccessor, [
+          '@type' => $type->name,
+          '@elementId' => $elementId,
+        ]);
+      }
+      if ($type === EcaElementType::Event && isset($lookup[EcaElementType::Event->getPlural()][$successorId])) {
+        $this->context->addViolation($constraint->disallowedSuccessorEvent, [
+          '@elementId' => $element['element_id'],
+        ]);
+      }
+    }
+  }
+
+}
diff --git a/modules/agents/src/Schema/Eca.php b/modules/agents/src/Schema/Eca.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e59476d0e433713e0d12294a75fa44f9c2ba660
--- /dev/null
+++ b/modules/agents/src/Schema/Eca.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Schema;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\schemata\Schema\SchemaInterface;
+
+/**
+ * Provides support for the ECA Model data type in Schemata.
+ */
+class Eca implements SchemaInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The data definition.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface
+   */
+  protected DataDefinitionInterface $definition;
+
+  /**
+   * Metadata values that describe the schema.
+   *
+   * @var array
+   */
+  protected array $metadata = [
+    'title' => 'ECA Model Schema',
+    'description' => 'The schema describing the properties of an ECA model.',
+  ];
+
+  /**
+   * Typed Data objects for all properties.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface[]
+   */
+  protected array $properties = [];
+
+  /**
+   * Constructs an ECA schema.
+   *
+   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
+   *   The Typed Data definition.
+   * @param array $properties
+   *   The properties.
+   */
+  public function __construct(DataDefinitionInterface $definition, array $properties = []) {
+    $this->definition = $definition;
+    $this->addProperties($properties);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addProperties(array $properties): void {
+    $this->properties += $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId(): string {
+    return 'eca_model';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleId(): string {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProperties(): array {
+    return $this->properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata(): array {
+    return $this->metadata;
+  }
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataProvider.php b/modules/agents/src/Services/DataProvider/DataProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..76dce05127963810e8447f3f9aa488bf620c0837
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataProvider.php
@@ -0,0 +1,257 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\eca\Attributes\Token;
+use Drupal\eca\Entity\Eca;
+use Drupal\eca\Service\Actions;
+use Drupal\eca\Service\Conditions;
+use Drupal\eca\Service\Modellers;
+use Drupal\token\TreeBuilderInterface;
+use Illuminate\Support\Arr;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Class for providing ECA-data.
+ */
+class DataProvider implements DataProviderInterface {
+
+  /**
+   * The view mode which determines how many details are returned.
+   *
+   * @var \Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum
+   */
+  protected DataViewModeEnum $viewMode = DataViewModeEnum::Teaser;
+
+  /**
+   * Constructs an DataProvider instance.
+   *
+   * @param \Drupal\eca\Service\Modellers $modellers
+   *   The service for ECA modellers.
+   * @param \Drupal\eca\Service\Conditions $conditions
+   *   The conditions.
+   * @param \Drupal\eca\Service\Actions $actions
+   *   The actions.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface $modelMapper
+   *   The model mapper.
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The normalizer.
+   * @param \Drupal\token\TreeBuilderInterface $treeBuilder
+   *   The token tree builder.
+   */
+  public function __construct(
+    protected Modellers $modellers,
+    protected Conditions $conditions,
+    protected Actions $actions,
+    protected EntityTypeManagerInterface $entityTypeManager,
+    protected ModelMapperInterface $modelMapper,
+    protected NormalizerInterface $normalizer,
+    protected TreeBuilderInterface $treeBuilder,
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEvents(): array {
+    $output = [];
+
+    foreach ($this->modellers->events() as $event) {
+      $info = [
+        'plugin_id' => $event->getPluginId(),
+        'name' => $event->getPluginDefinition()['label'],
+        'module' => $event->getPluginDefinition()['provider'],
+      ];
+
+      if ($this->viewMode === DataViewModeEnum::Full) {
+        $info['exposed_tokens'] = array_reduce($event->getTokens(), function (array $carry, Token $token) {
+          $info = [
+            'name' => $token->name,
+            'description' => $token->description,
+          ];
+          if (!empty($token->aliases)) {
+            $info['aliases'] = implode(', ', $token->aliases);
+          }
+          $carry[] = $info;
+
+          return $carry;
+        }, []);
+
+        $info['configuration'] = $this->buildConfig($event);
+      }
+
+      $output[] = $info;
+    }
+
+    return $output;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConditions(): array {
+    return $this->convertPlugins($this->conditions->conditions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActions(): array {
+    return $this->convertPlugins($this->actions->actions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getComponents(array $filterIds = []): array {
+    $filterFunction = fn ($plugin) => in_array($plugin['plugin_id'], $filterIds, TRUE);
+
+    return [
+      EcaElementType::Event->getPlural() => empty($filterIds) ? $this->getEvents() : array_values(array_filter($this->getEvents(), $filterFunction)),
+      EcaElementType::Condition->getPlural() => empty($filterIds) ? $this->getConditions() : array_values(array_filter($this->getConditions(), $filterFunction)),
+      EcaElementType::Action->getPlural() => empty($filterIds) ? $this->getActions() : array_values(array_filter($this->getActions(), $filterFunction)),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getModels(array $filterIds = []): array {
+    /** @var \Drupal\eca\Entity\EcaStorage $storage */
+    $storage = $this->entityTypeManager->getStorage('eca');
+    $models = $storage->loadMultiple();
+
+    if (!empty($filterIds)) {
+      $models = array_filter($models, function (Eca $model) use ($filterIds) {
+        return in_array($model->id(), $filterIds, TRUE);
+      });
+    }
+
+    return array_reduce($models, function (array $carry, Eca $eca) {
+      $model = $this->modelMapper->fromEntity($eca);
+      $data = array_filter($this->normalizer->normalize($model));
+
+      if ($this->viewMode === DataViewModeEnum::Teaser) {
+        $data = Arr::only($data, ['model_id', 'label', 'description']);
+      }
+
+      $carry[] = $data;
+
+      return $carry;
+    }, []);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTokens(): array {
+    $output = [];
+
+    $render = $this->treeBuilder->buildAllRenderable();
+
+    foreach ($render['#token_tree'] as $typeInfo) {
+      foreach ($typeInfo['tokens'] as $token => $tokenInfo) {
+        $output[$token] = [
+          'name' => $tokenInfo['name'],
+        ];
+        if (!empty($tokenInfo['description'])) {
+          $output[$token]['description'] = $tokenInfo['description'];
+        }
+      }
+    }
+
+    return $output;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setViewMode(DataViewModeEnum $viewMode): DataProviderInterface {
+    $this->viewMode = $viewMode;
+
+    return $this;
+  }
+
+  /**
+   * Converts a list of plugins to an array of essential information.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface[] $plugins
+   *   The list of plugins.
+   *
+   * @return array
+   *   Returns a converted list of plugins with essential information.
+   */
+  protected function convertPlugins(array $plugins): array {
+    $output = [];
+
+    foreach ($plugins as $plugin) {
+      $info = [
+        'plugin_id' => $plugin->getPluginId(),
+        'name' => (string) $plugin->getPluginDefinition()['label'],
+      ];
+
+      if ($this->viewMode === DataViewModeEnum::Full) {
+        $info['configuration'] = $this->buildConfig($plugin);
+      }
+
+      if (!empty($plugin->getPluginDefinition()['description'])) {
+        $info['description'] = (string) $plugin->getPluginDefinition()['description'];
+      }
+
+      $output[] = array_filter($info);
+    }
+
+    return $output;
+  }
+
+  /**
+   * Build the configuration details for the given plugin.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
+   *   The plugin.
+   *
+   * @return array
+   *   Returns the configuration details.
+   */
+  protected function buildConfig(PluginInspectionInterface $plugin): array {
+    if (!$plugin instanceof ConfigurableInterface && !$plugin instanceof PluginFormInterface) {
+      return [];
+    }
+
+    $defaultConfig = $plugin->defaultConfiguration();
+    $configKeys = array_keys($defaultConfig);
+    $elements = array_filter($plugin->buildConfigurationForm([], new FormState()), function ($key) use ($configKeys) {
+      return in_array($key, $configKeys);
+    }, ARRAY_FILTER_USE_KEY);
+
+    return array_reduce(array_keys($elements), function (array $carry, $key) use ($elements, $defaultConfig) {
+      $config = [
+        'config_id' => $key,
+        'name' => (string) $elements[$key]['#title'],
+      ];
+      if (!empty($elements[$key]['#description'])) {
+        $config['description'] = (string) $elements[$key]['#description'];
+      }
+      if (!empty($elements[$key]['#options'])) {
+        $config['options'] = array_keys($elements[$key]['#options']);
+      }
+      if (isset($defaultConfig[$key])) {
+        $config['value_type'] = gettype($defaultConfig[$key]);
+      }
+
+      $carry[] = $config;
+
+      return $carry;
+    }, []);
+  }
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataProviderInterface.php b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..85ce273d8d8eb17d4006204b07feb6d3c93302e5
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataProviderInterface.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+/**
+ * Interface for ECA data provider.
+ */
+interface DataProviderInterface {
+
+  /**
+   * Get all the events.
+   *
+   * @return array
+   *   Returns the events.
+   */
+  public function getEvents(): array;
+
+  /**
+   * Get all the conditions.
+   *
+   * @return array
+   *   Returns the conditions.
+   */
+  public function getConditions(): array;
+
+  /**
+   * Get all the actions.
+   *
+   * @return array
+   *   Returns the actions.
+   */
+  public function getActions(): array;
+
+  /**
+   * A wrapper that returns the events, conditions and actions.
+   *
+   * @param array $filterIds
+   *   An optional array of IDs to filter by.
+   *
+   * @return array
+   *   The collection of events, conditions and actions.
+   */
+  public function getComponents(array $filterIds = []): array;
+
+  /**
+   * Get models.
+   *
+   * @param array $filterIds
+   *   An optional array of IDs to filter by.
+   *
+   * @return array
+   *   Returns the models.
+   */
+  public function getModels(array $filterIds = []): array;
+
+  /**
+   * Get the available tokens.
+   *
+   * @return array
+   *   Return all the available tokens.
+   */
+  public function getTokens(): array;
+
+  /**
+   * Allows overriding the view mode.
+   *
+   * This is used for controlling how much details are exposed for the different
+   * components.
+   *
+   * @param \Drupal\ai_eca_agents\Services\DataProvider\DataViewModeEnum $viewMode
+   *   The view mode.
+   *
+   * @return \Drupal\ai_eca_agents\Services\DataProvider\DataProviderInterface
+   *   Returns the altered instance.
+   */
+  public function setViewMode(DataViewModeEnum $viewMode): DataProviderInterface;
+
+}
diff --git a/modules/agents/src/Services/DataProvider/DataViewModeEnum.php b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a63783405dc107a69cdf167e22da83639c85a57
--- /dev/null
+++ b/modules/agents/src/Services/DataProvider/DataViewModeEnum.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\DataProvider;
+
+/**
+ * Enumeration determining the view mode of the data.
+ */
+enum DataViewModeEnum: string {
+
+  case Teaser = 'teaser';
+  case Full = 'full';
+
+}
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepository.php b/modules/agents/src/Services/EcaRepository/EcaRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb39e1453990bcd609e2621db1e25a06248071eb
--- /dev/null
+++ b/modules/agents/src/Services/EcaRepository/EcaRepository.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\EcaRepository;
+
+use Drupal\ai_eca_agents\EntityViolationException;
+use Drupal\ai_eca_agents\MissingEventException;
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Repository for the ECA entity type.
+ */
+class EcaRepository implements EcaRepositoryInterface {
+
+  /**
+   * Constructs an EcaHelper-instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface $modelMapper
+   *   The model mapper.
+   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
+   *   The typed data manager.
+   */
+  public function __construct(
+    protected EntityTypeManagerInterface $entityTypeManager,
+    protected ModelMapperInterface $modelMapper,
+    protected TypedDataManagerInterface $typedDataManager,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(string $id): ?Eca {
+    return $this->entityTypeManager->getStorage('eca')
+      ->load($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $data, bool $save = TRUE, ?string $id = NULL): Eca {
+    /** @var \Drupal\eca\Entity\EcaStorage $storage */
+    $storage = $this->entityTypeManager->getStorage('eca');
+    /** @var \Drupal\eca\Entity\Eca $eca */
+    $eca = $storage->create();
+    if (!empty($id)) {
+      $eca = $storage->load($id);
+    }
+
+    // Convert the given data to the ECA-model.
+    $model = $this->modelMapper->fromPayload($data);
+
+    // Map the model to the entity.
+    $random = new Random();
+    $idFallback = $model->get('model_id')?->getString() ?? sprintf('process_%s', $random->name(7));
+    $eca->set('id', $id ?? $idFallback);
+    $eca->set('label', $model->get('label')->getString());
+    $eca->set('modeller', 'fallback');
+    $eca->set('version', $eca->get('version') ?? '0.0.1');
+    $eca->setStatus(FALSE);
+    $eca->resetComponents();
+
+    // Set events.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
+    foreach ($model->get('events') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
+        $successor['condition'] ??= '';
+        unset($successor['element_id']);
+      }
+
+      $eca->addEvent(
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
+        $plugin->get('label')->getString(),
+        $plugin->get('configuration')->getValue(),
+        $successors
+      );
+    }
+
+    // Set conditions.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $plugin */
+    foreach ($model->get('conditions') as $plugin) {
+      $eca->addCondition(
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
+        $plugin->get('configuration')->getValue(),
+      );
+    }
+
+    // Set gateways.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaGateway $plugin */
+    foreach ($model->get('gateways') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
+        $successor['condition'] ??= '';
+        unset($successor['element_id']);
+      }
+
+      $eca->addGateway(
+        $plugin->get('gateway_id')->getString(),
+        $plugin->get('type')->getValue(),
+        $successors
+      );
+    }
+
+    // Set actions.
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaGateway $plugin */
+    foreach ($model->get('actions') as $plugin) {
+      $successors = $plugin->get('successors')->getValue() ?? [];
+      foreach ($successors as &$successor) {
+        $successor['id'] = $successor['element_id'];
+        $successor['condition'] ??= '';
+        unset($successor['element_id']);
+      }
+
+      $eca->addAction(
+        $plugin->get('element_id')->getString(),
+        $plugin->get('plugin_id')->getString(),
+        $plugin->get('label')->getString(),
+        $plugin->get('configuration')->getValue() ?? [],
+        $successors
+      );
+    }
+
+    // Validate the entity.
+    $definition = $this->typedDataManager->createDataDefinition(sprintf('entity:%s', $eca->getEntityTypeId()));
+    $violations = $this->typedDataManager->create($definition, $eca)
+      ->validate();
+    if ($violations->count()) {
+      throw new EntityViolationException('', 0, NULL, $violations);
+    }
+    if (empty($eca->getUsedEvents())) {
+      throw new MissingEventException('No events registered.');
+    }
+
+    // Save the entity.
+    if ($save) {
+      $eca->save();
+    }
+
+    return $eca;
+  }
+
+}
diff --git a/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1f571e4eb13105abfb2ddc0573c6b3456346de2
--- /dev/null
+++ b/modules/agents/src/Services/EcaRepository/EcaRepositoryInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\EcaRepository;
+
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Repository for the ECA entity type.
+ */
+interface EcaRepositoryInterface {
+
+  /**
+   * Load a model by ID.
+   *
+   * @param string $id
+   *   The ID to look for.
+   *
+   * @return \Drupal\eca\Entity\Eca|null
+   *   The ECA-entity or NULL.
+   */
+  public function get(string $id): ?Eca;
+
+  /**
+   * Build the ECA-model.
+   *
+   * @param array $data
+   *   The data to use.
+   * @param bool $save
+   *   Toggle to save the model.
+   * @param string|null $id
+   *   The ID of an existing model to load.
+   *
+   * @return \Drupal\eca\Entity\Eca
+   *   Returns the ECA-model.
+   */
+  public function build(array $data, bool $save = TRUE, ?string $id = NULL): Eca;
+
+}
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapper.php b/modules/agents/src/Services/ModelMapper/ModelMapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..28f9ef766ac9925a28bad97b7b123659c1b82e26
--- /dev/null
+++ b/modules/agents/src/Services/ModelMapper/ModelMapper.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\ModelMapper;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
+use Drupal\ai_eca_agents\TypedData\EcaGatewayDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaPluginDefinition;
+use Drupal\ai_eca_agents\TypedData\EcaSuccessorDefinition;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\eca\Entity\Eca;
+use Drupal\eca\Entity\Objects\EcaEvent;
+use Illuminate\Support\Arr;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * The model mapper.
+ */
+class ModelMapper implements ModelMapperInterface {
+
+  /**
+   * The model mapper.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
+   *   The typed data manager.
+   * @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
+   *   The normalizer.
+   */
+  public function __construct(
+    protected TypedDataManagerInterface $typedDataManager,
+    protected NormalizerInterface $normalizer,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fromPayload(array $payload): EcaModel {
+    $definition = EcaModelDefinition::create();
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaModel $model */
+    $model = $this->typedDataManager->create($definition, $payload);
+
+    /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */
+    $violations = $model->validate();
+    if (count($violations) > 0) {
+      throw new MissingDataException($this->formatValidationMessages($model, $violations));
+    }
+
+    return $model;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fromEntity(Eca $entity): EcaModel {
+    $modelDef = EcaModelDefinition::create();
+    /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaModel $model */
+    $model = $this->typedDataManager->create($modelDef);
+
+    // Basic properties.
+    $model->set('model_id', $entity->id());
+    $model->set('label', $entity->label());
+    if (!empty($entity->getModel()->getDocumentation())) {
+      $model->set('description', $entity->getModel()->getDocumentation());
+    }
+
+    // Events.
+    $plugins = array_reduce($entity->getUsedEvents(), function (array $carry, EcaEvent $event) {
+      $successors = $this->mapSuccessorsToModel($event->getSuccessors());
+
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaElementType::Event);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('element_id', $event->getId());
+      $model->set('plugin_id', $event->getPlugin()->getPluginId());
+      $model->set('label', $event->getLabel());
+      $model->set('configuration', $event->getConfiguration());
+      $model->set('successors', $successors);
+
+      $carry[] = $this->normalizer->normalize($model);
+
+      return $carry;
+    }, []);
+    $model->set('events', $plugins);
+
+    // Conditions.
+    $conditions = $entity->getConditions();
+    $plugins = array_reduce(array_keys($conditions), function (array $carry, string $conditionId) use ($conditions) {
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaElementType::Condition);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('element_id', $conditionId);
+      $model->set('plugin_id', $conditions[$conditionId]['plugin']);
+      $model->set('configuration', $conditions[$conditionId]['configuration']);
+
+      $carry[] = $this->normalizer->normalize($model);
+
+      return $carry;
+    }, []);
+    $model->set('conditions', $plugins);
+
+    // Actions.
+    $actions = $entity->getActions();
+    $plugins = array_reduce(array_keys($actions), function (array $carry, string $actionId) use ($actions) {
+      $successors = $this->mapSuccessorsToModel($actions[$actionId]['successors']);
+
+      $def = EcaPluginDefinition::create();
+      $def->setSetting('data_type', EcaElementType::Action);
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create($def);
+      $model->set('element_id', $actionId);
+      $model->set('plugin_id', $actions[$actionId]['plugin']);
+      $model->set('label', $actions[$actionId]['label']);
+      $model->set('configuration', $actions[$actionId]['configuration']);
+      $model->set('successors', $successors);
+
+      $carry[] = $this->normalizer->normalize($model);
+
+      return $carry;
+    }, []);
+    $model->set('actions', $plugins);
+
+    // Gateways.
+    $gateways = $entity->get('gateways');
+    $plugins = array_reduce(array_keys($gateways), function (array $carry, string $gatewayId) use ($gateways) {
+      $successors = $this->mapSuccessorsToModel($gateways[$gatewayId]['successors']);
+
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaPlugin $model */
+      $model = $this->typedDataManager->create(EcaGatewayDefinition::create());
+      $model->set('gateway_id', $gatewayId);
+      $model->set('type', $gateways[$gatewayId]['type']);
+      $model->set('successors', $successors);
+
+      $carry[] = $this->normalizer->normalize($model);
+
+      return $carry;
+    }, []);
+    $model->set('gateways', $plugins);
+
+    return $model;
+  }
+
+  /**
+   * Format the violation messages in a human-readable way.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
+   *   The typed data object.
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
+   *   The violation list.
+   *
+   * @return string
+   *   Returns the formatted violation messages.
+   */
+  protected function formatValidationMessages(ComplexDataInterface $data, ConstraintViolationListInterface $violations): string {
+    $lines = [
+      sprintf('There were validation errors in %s:', $data->getName()),
+    ];
+    /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
+    foreach ($violations as $violation) {
+      $message = sprintf('- %s', $violation->getMessage());
+      if (!empty($violation->getPropertyPath())) {
+        $message = sprintf('- %s: %s', $violation->getPropertyPath(), $violation->getMessage());
+      }
+      $lines[] = $message;
+    }
+
+    return implode("\n", $lines);
+  }
+
+  /**
+   * Map an array of successor-data to the EcaSuccessor data type.
+   *
+   * @param array $successors
+   *   The array of successor-data.
+   *
+   * @return array
+   *   Returns a normalized version of the EcaSuccessor data type.
+   *
+   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
+   * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
+   */
+  protected function mapSuccessorsToModel(array $successors): array {
+    return array_filter(array_reduce($successors, function ($carry, array $successor) {
+      /** @var \Drupal\ai_eca_agents\Plugin\DataType\EcaSuccessor $model */
+      $model = $this->typedDataManager->create(EcaSuccessorDefinition::create());
+      $model->set('element_id', $successor['id']);
+      $model->set('condition', $successor['condition']);
+
+      $carry[] = Arr::whereNotNull($this->normalizer->normalize($model));
+
+      return $carry;
+    }, []));
+  }
+
+}
diff --git a/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..da0af27bb5f96840c76c64bd0127553b01c3c2e0
--- /dev/null
+++ b/modules/agents/src/Services/ModelMapper/ModelMapperInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\ai_eca_agents\Services\ModelMapper;
+
+use Drupal\ai_eca_agents\Plugin\DataType\EcaModel;
+use Drupal\eca\Entity\Eca;
+
+/**
+ * Interface that describes a model mapper.
+ */
+interface ModelMapperInterface {
+
+  /**
+   * Map a payload to the ECA Model typed data.
+   *
+   * @param array $payload
+   *   The external payload.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\DataType\EcaModel
+   *   Returns the ECA Model typed data.
+   */
+  public function fromPayload(array $payload): EcaModel;
+
+  /**
+   * Map an ECA-entity to the ECA Model typed data.
+   *
+   * @param \Drupal\eca\Entity\Eca $entity
+   *   The ECA entity.
+   *
+   * @return \Drupal\ai_eca_agents\Plugin\DataType\EcaModel
+   *   Returns the ECA Model typed data.
+   */
+  public function fromEntity(Eca $entity): EcaModel;
+
+}
diff --git a/modules/agents/src/TypedData/EcaGatewayDefinition.php b/modules/agents/src/TypedData/EcaGatewayDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..4ba96292c2c40eed3ee10f2aff6f77661e64a8f9
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaGatewayDefinition.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\ListDataDefinition;
+
+/**
+ * Definition of the ECA Gateway data type.
+ */
+class EcaGatewayDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+
+    $properties['gateway_id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID of the element'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    $properties['type'] = DataDefinition::create('integer')
+      ->setLabel(new TranslatableMarkup('Type'))
+      ->setSetting('allowed_values_function', '')
+      ->setSetting('allowed_values', [0])
+      ->addConstraint('Choice', [
+        'choices' => [0],
+      ]);
+
+    $properties['successors'] = ListDataDefinition::create('eca_successor')
+      ->setLabel('Successors');
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_gateway'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaModelDefinition.php b/modules/agents/src/TypedData/EcaModelDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a40b5b5d66e92e4e4b60cc00a74c61796e925a8
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaModelDefinition.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\ListDataDefinition;
+
+/**
+ * Definition of the ECA Model data type.
+ */
+class EcaModelDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values = []) {
+    parent::__construct($values);
+
+    $this->addConstraint('SuccessorsAreValid');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+
+    $properties['model_id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    $properties['label'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Label'))
+      ->setRequired(TRUE)
+      ->addConstraint('Length', ['max' => 255])
+      ->addConstraint('NotBlank');
+
+    $properties['version'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Version'));
+
+    $properties['description'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Description'));
+
+    $properties['events'] = ListDataDefinition::create('eca_plugin')
+      ->setLabel(new TranslatableMarkup('Events'))
+      ->setRequired(TRUE)
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaElementType::Event)
+      )
+      ->addConstraint('NotNull');
+
+    $properties['conditions'] = ListDataDefinition::create('eca_plugin')
+      ->setLabel(new TranslatableMarkup('Conditions'))
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaElementType::Condition)
+      );
+
+    $properties['gateways'] = ListDataDefinition::create('eca_gateway')
+      ->setLabel(new TranslatableMarkup('Gateways'));
+
+    $properties['actions'] = ListDataDefinition::create('eca_plugin')
+      ->setLabel(new TranslatableMarkup('Actions'))
+      ->setRequired(TRUE)
+      ->setItemDefinition(
+        EcaPluginDefinition::create()
+          ->setSetting('data_type', EcaElementType::Action)
+      )
+      ->addConstraint('NotNull');
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_model'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaPluginDefinition.php b/modules/agents/src/TypedData/EcaPluginDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..364e9361705de1676c5dcd5b37b6f522ef332710
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaPluginDefinition.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\ai_eca_agents\EcaElementType;
+use Drupal\Core\Action\ActionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\ListDataDefinition;
+use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
+use Drupal\eca\Plugin\ECA\Event\EventInterface;
+
+/**
+ * Definition of the ECA Plugin data type.
+ */
+class EcaPluginDefinition extends ComplexDataDefinitionBase {
+
+  protected const PROP_LABEL = 'label';
+
+  protected const PROP_SUCCESSORS = 'successors';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+
+    $properties['element_id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('ID of the element'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    $properties['plugin_id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Plugin ID'))
+      ->setRequired(TRUE)
+      ->addConstraint('PluginExists', [
+        'manager' => $this->getPluginManagerId(),
+        'interface' => $this->getPluginInterface(),
+      ]);
+
+    if (in_array(self::PROP_LABEL, $this->getEnabledProperties(), TRUE)) {
+      $properties['label'] = DataDefinition::create('string')
+        ->setLabel(new TranslatableMarkup('Label'))
+        ->setRequired(TRUE)
+        ->addConstraint('Regex', [
+          'pattern' => '/([^\PC\x09\x0a\x0d])/u',
+          'match' => FALSE,
+          'message' => 'Text is not allowed to contain control characters, only visible characters.',
+        ]);
+    }
+
+    $properties['configuration'] = DataDefinition::create('any');
+
+    if (in_array(self::PROP_SUCCESSORS, $this->getEnabledProperties(), TRUE)) {
+      $properties['successors'] = ListDataDefinition::create('eca_successor')
+        ->setLabel(new TranslatableMarkup('Successors'));
+    }
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_plugin'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+  /**
+   * Get the plugin manager ID.
+   *
+   * @return string
+   *   Returns the plugin manager ID.
+   */
+  protected function getPluginManagerId(): string {
+    return match ($this->getSetting('data_type')) {
+      EcaElementType::Action => 'plugin.manager.action',
+      EcaElementType::Event => 'plugin.manager.eca.event',
+      EcaElementType::Condition => 'plugin.manager.eca.condition',
+      default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin manager ID.', [
+        '@type' => $this->getSetting('data_type'),
+      ])),
+    };
+  }
+
+  /**
+   * Get the plugin interface.
+   *
+   * @return string
+   *   Returns the plugin interface.
+   */
+  protected function getPluginInterface(): string {
+    return match ($this->getSetting('data_type')) {
+      EcaElementType::Action => ActionInterface::class,
+      EcaElementType::Event => EventInterface::class,
+      EcaElementType::Condition => ConditionInterface::class,
+      default => throw new \InvalidArgumentException(t('Could not match data type @type to plugin interface.', [
+        '@type' => $this->getSetting('data_type'),
+      ])),
+    };
+  }
+
+  /**
+   * Get a list of enabled properties.
+   *
+   * @return string[]
+   *   The list of enabled properties.
+   */
+  protected function getEnabledProperties(): array {
+    $default = [
+      self::PROP_LABEL,
+      self::PROP_SUCCESSORS,
+    ];
+
+    return match ($this->getSetting('data_type')) {
+      EcaElementType::Condition => [],
+      default => $default,
+    };
+  }
+
+}
diff --git a/modules/agents/src/TypedData/EcaSuccessorDefinition.php b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..b797cf4e3167ed920519a60f29f11284e6c760d8
--- /dev/null
+++ b/modules/agents/src/TypedData/EcaSuccessorDefinition.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\ai_eca_agents\TypedData;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+
+/**
+ * Definition of the ECA Successor data type.
+ */
+class EcaSuccessorDefinition extends ComplexDataDefinitionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions(): array {
+    $properties = [];
+
+    $properties['element_id'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('The ID of an existing action or gateway.'))
+      ->setRequired(TRUE)
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    $properties['condition'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('The ID of an existing condition.'))
+      ->addConstraint('Regex', [
+        'pattern' => '/^[\w]+$/',
+        'message' => 'The %value ID is not valid.',
+      ]);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create($type = 'eca_successor'): DataDefinitionInterface {
+    return new self(['type' => $type]);
+  }
+
+}
diff --git a/modules/agents/tests/assets/from_payload_0.json b/modules/agents/tests/assets/from_payload_0.json
new file mode 100644
index 0000000000000000000000000000000000000000..3425c797d1d69153c20cc49d55d8ccba138d8e70
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_0.json
@@ -0,0 +1,91 @@
+{
+  "model_id": "process_1",
+  "label": "Create Article on Page Publish",
+  "events": [
+    {
+      "element_id": "event_1",
+      "plugin_id": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "element_id": "action_2"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "element_id": "action_3"
+        }
+      ]
+    },
+    {
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "element_id": "action_4"
+        }
+      ]
+    },
+    {
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "element_id": "action_5"
+        }
+      ]
+    },
+    {
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "element_id": "action_6"
+        }
+      ]
+    },
+    {
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_1.json b/modules/agents/tests/assets/from_payload_1.json
new file mode 100644
index 0000000000000000000000000000000000000000..fb33b80ec5288b2dbc39de4fd4d051412ea9c7d1
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_1.json
@@ -0,0 +1,89 @@
+{
+  "events": [
+    {
+      "element_id": "event_1",
+      "plugin_id": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "element_id": "action_2"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "element_id": "action_3"
+        }
+      ]
+    },
+    {
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "element_id": "action_4"
+        }
+      ]
+    },
+    {
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "element_id": "action_5"
+        }
+      ]
+    },
+    {
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "element_id": "action_6"
+        }
+      ]
+    },
+    {
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_2.json b/modules/agents/tests/assets/from_payload_2.json
new file mode 100644
index 0000000000000000000000000000000000000000..98bbba1c070d7e1f1506b05b18877343b661e24c
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_2.json
@@ -0,0 +1,76 @@
+{
+  "model_id": "process_1",
+  "label": "Create Article on Page Publish",
+  "actions": [
+    {
+      "element_id": "action_2",
+      "plugin_id": "eca_token_set_value",
+      "label": "Set Page Title Token",
+      "configuration": {
+        "token_name": "page_title",
+        "token_value": "[entity:title]",
+        "use_yaml": false
+      },
+      "successors": [
+        {
+          "element_id": "action_3"
+        }
+      ]
+    },
+    {
+      "element_id": "action_3",
+      "plugin_id": "eca_token_load_user_current",
+      "label": "Load Author Info",
+      "configuration": {
+        "token_name": "author_info"
+      },
+      "successors": [
+        {
+          "element_id": "action_4"
+        }
+      ]
+    },
+    {
+      "element_id": "action_4",
+      "plugin_id": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "New Article: [page_title]",
+        "published": true,
+        "owner": "[author_info:uid]"
+      },
+      "successors": [
+        {
+          "element_id": "action_5"
+        }
+      ]
+    },
+    {
+      "element_id": "action_5",
+      "plugin_id": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "New article based on page '[page_title]'. Authored by [author_info:name]. Static text: Example static content.",
+        "method": "set:force_clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": [
+        {
+          "element_id": "action_6"
+        }
+      ]
+    },
+    {
+      "element_id": "action_6",
+      "plugin_id": "eca_save_entity",
+      "label": "Save Article",
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_3.json b/modules/agents/tests/assets/from_payload_3.json
new file mode 100644
index 0000000000000000000000000000000000000000..c564472f5ebccf227f4ff340732b7ac41ea68056
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_3.json
@@ -0,0 +1,92 @@
+{
+  "model_id": "create_article_on_new_page",
+  "label": "Create Article on New Page",
+  "version": "v1",
+  "events": [
+    {
+      "element_id": "start_event",
+      "plugin_id": "content_entity:insert",
+      "label": "New Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "element_id": "extract_page_title"
+        }
+      ]
+    }
+  ],
+  "conditions": [
+    {
+      "element_id": "check_title_for_ai",
+      "plugin_id": "eca_entity_field_value",
+      "configuration": {
+        "field_name": "title",
+        "expected_value": "AI",
+        "operator": "contains",
+        "type": "value",
+        "case": false,
+        "negate": false,
+        "entity": "entity"
+      }
+    }
+  ],
+  "gateways": [
+    {
+      "gateway_id": "title_check_gateway",
+      "type": 0,
+      "successors": [
+        {
+          "element_id": "create_article_unpublished",
+          "condition": "check_title_for_ai"
+        },
+        {
+          "element_id": "create_article_published"
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "element_id": "extract_page_title",
+      "plugin_id": "eca_get_field_value",
+      "label": "Extract Page Title",
+      "configuration": {
+        "field_name": "title",
+        "token_name": "page_title"
+      },
+      "successors": [
+        {
+          "element_id": "title_check_gateway"
+        }
+      ]
+    },
+    {
+      "element_id": "create_article_unpublished",
+      "plugin_id": "eca_new_entity",
+      "label": "Create Unpublished Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article about: {page_title}",
+        "published": false,
+        "owner": "{{session_user.uid}}"
+      }
+    },
+    {
+      "element_id": "create_article_published",
+      "plugin_id": "eca_new_entity",
+      "label": "Create Published Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article about: {page_title}",
+        "published": true,
+        "owner": "{{session_user.uid}}"
+      }
+    }
+  ]
+}
diff --git a/modules/agents/tests/assets/from_payload_4.json b/modules/agents/tests/assets/from_payload_4.json
new file mode 100644
index 0000000000000000000000000000000000000000..5eca026dcee80add0fccd84f277a1db43c73bef1
--- /dev/null
+++ b/modules/agents/tests/assets/from_payload_4.json
@@ -0,0 +1,105 @@
+{
+  "model_id": "create_article_from_page",
+  "label": "Create Article From Page Publication",
+  "events": [
+    {
+      "element_id": "event_page_published",
+      "plugin_id": "content_entity:insert",
+      "label": "Page Published",
+      "configuration": {
+        "type": "node page"
+      },
+      "successors": [
+        {
+          "element_id": "condition_check_title",
+          "condition": ""
+        }
+      ]
+    }
+  ],
+  "conditions": [
+    {
+      "element_id": "condition_check_title",
+      "plugin_id": "eca_entity_field_value",
+      "configuration": {
+        "field_name": "title",
+        "expected_value": "AI",
+        "operator": "contains",
+        "type": "value",
+        "case": false,
+        "negate": false,
+        "entity": "event.entity"
+      }
+    }
+  ],
+  "gateways": [
+    {
+      "gateway_id": "gateway_title_check",
+      "type": 0,
+      "successors": [
+        {
+          "element_id": "action_create_article_offline",
+          "condition": "condition_check_title"
+        },
+        {
+          "element_id": "action_create_article",
+          "condition": ""
+        }
+      ]
+    }
+  ],
+  "actions": [
+    {
+      "element_id": "action_create_article",
+      "plugin_id": "eca_new_entity",
+      "label": "Create New Article",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article for {{event.entity.title}} by {{event.entity.owner}}",
+        "published": true,
+        "owner": "{{event.entity.owner}}"
+      },
+      "successors": [
+        {
+          "element_id": "action_set_article_field",
+          "condition": ""
+        }
+      ]
+    },
+    {
+      "element_id": "action_create_article_offline",
+      "plugin_id": "eca_new_entity",
+      "label": "Create New Article Offline",
+      "configuration": {
+        "token_name": "new_article",
+        "type": "node article",
+        "langcode": "en",
+        "label": "Article for {{event.entity.title}} by {{event.entity.owner}}",
+        "published": false,
+        "owner": "{{event.entity.owner}}"
+      },
+      "successors": [
+        {
+          "element_id": "action_set_article_field",
+          "condition": ""
+        }
+      ]
+    },
+    {
+      "element_id": "action_set_article_field",
+      "plugin_id": "eca_set_field_value",
+      "label": "Set Article Body",
+      "configuration": {
+        "field_name": "body.value",
+        "field_value": "This article was auto-generated from the page titled '{{event.entity.title}}', authored by {{event.entity.owner}}. Additional static information about the author can be included here.",
+        "method": "set:clear",
+        "strip_tags": false,
+        "trim": true,
+        "save_entity": true
+      },
+      "successors": []
+    }
+  ]
+}
diff --git a/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..94fa524ef98df781eeef6d5c79a8e9e5d2a14a4e
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/AiEcaAgentsKernelTestBase.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Base class for Kernel-tests regarding AI ECA Agents.
+ */
+abstract class AiEcaAgentsKernelTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'eca_test_model_cross_ref',
+    'eca_user',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'serialization',
+    'schemata',
+    'schemata_json_schema',
+    'system',
+  ];
+
+  /**
+   * Generate different sets of data payloads.
+   *
+   * @return \Generator
+   *   Returns a collection of data payloads.
+   */
+  public static function payloadProvider(): \Generator {
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_0.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 0,
+        'actions' => 5,
+        'label' => 'Create Article on Page Publish',
+      ],
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_1.json', __DIR__))),
+      [],
+      NULL,
+      'model_id: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_2.json', __DIR__))),
+      [],
+      NULL,
+      'events: This value should not be null',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+      ],
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_3.json', __DIR__))),
+      [
+        'events' => 1,
+        'conditions' => 1,
+        'gateways' => 1,
+        'actions' => 3,
+        'label' => 'Create Article on New Page',
+        'version' => 'v1',
+      ],
+      'eca_test_0001',
+    ];
+
+    yield [
+      Json::decode(file_get_contents(sprintf('%s/../../assets/from_payload_4.json', __DIR__))),
+      [],
+      NULL,
+      "Invalid successor ID 'condition_check_title' for Event 'event_page_published'. Must reference a gateway or action.",
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('eca');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    // Create the Page content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
+    $node_type->save();
+    node_add_body_field($node_type);
+
+    // Create the Article content type with a standard body field.
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
+    $node_type->save();
+    node_add_body_field($node_type);
+  }
+
+}
diff --git a/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..95ca0cdd653ad4093af226ed622d45d07149d65a
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/EcaModelSchemaTest.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\ai_eca_agents\Schema\Eca as EcaSchema;
+use Drupal\ai_eca_agents\TypedData\EcaModelDefinition;
+use Drupal\KernelTests\KernelTestBase;
+use Spatie\Snapshots\MatchesSnapshots;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Kernel test for the ECA Model data type.
+ *
+ * @group ai_eca_agents
+ */
+class EcaModelSchemaTest extends KernelTestBase {
+
+  use MatchesSnapshots;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'serialization',
+    'schemata',
+    'schemata_json_schema',
+    'system',
+    'token',
+    'user',
+  ];
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface|null
+   */
+  protected ?SerializerInterface $serializer;
+
+  /**
+   * Validates the generated schema of the Eca Model data type.
+   */
+  public function testSchema(): void {
+    $definition = EcaModelDefinition::create();
+    $schema = new EcaSchema($definition, $definition->getPropertyDefinitions());
+    $schema = $this->serializer->serialize($schema, 'schema_json:json', []);
+
+    $this->assertMatchesJsonSnapshot($schema);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+
+    $this->installConfig(static::$modules);
+
+    $this->serializer = $this->container->get('serializer');
+  }
+
+}
diff --git a/modules/agents/tests/src/Kernel/EcaRepositoryTest.php b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a82ca3a3ba7a95a5adc2a2a97d705fa2015b223f
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/EcaRepositoryTest.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface;
+
+/**
+ * Tests various input data for generating ECA models.
+ *
+ * @group ai_eca_agents
+ */
+class EcaRepositoryTest extends AiEcaAgentsKernelTestBase {
+
+  /**
+   * The ECA repository.
+   *
+   * @var \Drupal\ai_eca_agents\Services\EcaRepository\EcaRepositoryInterface|null
+   */
+  protected ?EcaRepositoryInterface $ecaRepository;
+
+  /**
+   * Build an ECA-model with the provided data.
+   *
+   * @dataProvider payloadProvider
+   */
+  public function testBuildModel(array $data, array $assertions, ?string $id = NULL, ?string $errorMessage = NULL): void {
+    if (!empty($errorMessage)) {
+      $this->expectExceptionMessage($errorMessage);
+    }
+
+    $eca = $this->ecaRepository->build($data, TRUE, $id);
+
+    foreach ($assertions as $property => $expected) {
+      switch (TRUE) {
+        case is_int($expected):
+          $this->assertCount($expected, $eca->get($property));
+          break;
+
+        case is_string($expected):
+          $this->assertEquals($expected, $eca->get($property));
+          break;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->ecaRepository = \Drupal::service('ai_eca_agents.services.eca_repository');
+  }
+
+}
diff --git a/modules/agents/tests/src/Kernel/ModelMapperTest.php b/modules/agents/tests/src/Kernel/ModelMapperTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f13f697cd1dc2147524c3929ed3c0ea4e8eba54
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/ModelMapperTest.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace Drupal\Tests\ai_eca_agents\Kernel;
+
+use Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Spatie\Snapshots\MatchesSnapshots;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Tests various input data for generation ECA Model typed data.
+ *
+ * @group ai_eca_agents
+ */
+class ModelMapperTest extends AiEcaAgentsKernelTestBase {
+
+  use MatchesSnapshots;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ai_eca',
+    'ai_eca_agents',
+    'eca',
+    'eca_base',
+    'eca_content',
+    'eca_user',
+    'eca_test_model_cross_ref',
+    'eca_test_model_entity_basics',
+    'eca_test_model_set_field_value',
+    'field',
+    'node',
+    'text',
+    'token',
+    'user',
+    'serialization',
+    'schemata',
+    'schemata_json_schema',
+    'system',
+    'taxonomy',
+  ];
+
+  /**
+   * The model mapper.
+   *
+   * @var \Drupal\ai_eca_agents\Services\ModelMapper\ModelMapperInterface|null
+   */
+  protected ?ModelMapperInterface $modelMapper;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
+   */
+  protected ?EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The normalizer.
+   *
+   * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface|null
+   */
+  protected ?NormalizerInterface $normalizer;
+
+  /**
+   * Build an ECA Model type data object with the provided payloads.
+   *
+   * @dataProvider payloadProvider
+   */
+  public function testMappingFromPayload(array $payload, array $assertions, ?string $id = NULL, ?string $errorMessage = NULL): void {
+    if (!empty($errorMessage)) {
+      $this->expectExceptionMessage($errorMessage);
+    }
+
+    $model = $this->modelMapper->fromPayload($payload);
+
+    foreach ($assertions as $property => $expected) {
+      switch (TRUE) {
+        case is_int($expected):
+          $this->assertCount($expected, $model->get($property)->getValue());
+          break;
+
+        case is_string($expected):
+          $this->assertEquals($expected, $model->get($property)->getString());
+          break;
+      }
+    }
+  }
+
+  /**
+   * Build an ECA Model data type object with the provided entities.
+   *
+   * @dataProvider entityProvider
+   */
+  public function testMappingFromEntity(string $entityId): void {
+    /** @var \Drupal\eca\Entity\Eca $entity */
+    $entity = $this->entityTypeManager->getStorage('eca')
+      ->load($entityId);
+    $model = $this->modelMapper->fromEntity($entity);
+    $data = $this->normalizer->normalize($model);
+
+    $this->assertMatchesJsonSnapshot(json_encode($data, JSON_PRETTY_PRINT));
+  }
+
+  /**
+   * Generate different sets of ECA entity IDs.
+   *
+   * @return \Generator
+   *   Returns a collection of ECA entity IDs.
+   */
+  public static function entityProvider(): \Generator {
+    yield [
+      'eca_test_0001',
+    ];
+
+    yield [
+      'eca_test_0002',
+    ];
+
+    yield [
+      'eca_test_0009',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->modelMapper = \Drupal::service('ai_eca_agents.services.model_mapper');
+    $this->entityTypeManager = \Drupal::entityTypeManager();
+    $this->normalizer = \Drupal::service('serializer');
+  }
+
+}
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
new file mode 100644
index 0000000000000000000000000000000000000000..f7eab86e29210876b61c62c079c2a949b1695163
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/__snapshots__/EcaModelSchemaTest__testSchema__1.json
@@ -0,0 +1,198 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "id": "http://localhost/schemata/eca_model?_format=schema_json&_describes=json",
+    "type": "object",
+    "title": "ECA Model Schema",
+    "description": "The schema describing the properties of an ECA model.",
+    "properties": {
+        "model_id": {
+            "type": "string",
+            "title": "ID"
+        },
+        "label": {
+            "type": "string",
+            "title": "Label"
+        },
+        "version": {
+            "type": "string",
+            "title": "Version"
+        },
+        "description": {
+            "type": "string",
+            "title": "Description"
+        },
+        "events": {
+            "type": "array",
+            "title": "Events",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "element_id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin_id": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "label": {
+                        "type": "string",
+                        "title": "Label"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "element_id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "element_id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "element_id",
+                    "plugin_id",
+                    "label"
+                ]
+            },
+            "minItems": 1
+        },
+        "conditions": {
+            "type": "array",
+            "title": "Conditions",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "element_id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin_id": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    }
+                },
+                "required": [
+                    "element_id",
+                    "plugin_id"
+                ]
+            }
+        },
+        "gateways": {
+            "type": "array",
+            "title": "Gateways",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "gateway_id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "type": {
+                        "type": "integer",
+                        "title": "Type",
+                        "enum": [
+                            0
+                        ]
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "element_id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "element_id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "gateway_id"
+                ]
+            }
+        },
+        "actions": {
+            "type": "array",
+            "title": "Actions",
+            "items": {
+                "type": "object",
+                "properties": {
+                    "element_id": {
+                        "type": "string",
+                        "title": "ID of the element"
+                    },
+                    "plugin_id": {
+                        "type": "string",
+                        "title": "Plugin ID"
+                    },
+                    "label": {
+                        "type": "string",
+                        "title": "Label"
+                    },
+                    "configuration": {
+                        "type": "any"
+                    },
+                    "successors": {
+                        "type": "array",
+                        "title": "Successors",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "element_id": {
+                                    "type": "string",
+                                    "title": "The ID of an existing action or gateway."
+                                },
+                                "condition": {
+                                    "type": "string",
+                                    "title": "The ID of an existing condition."
+                                }
+                            },
+                            "required": [
+                                "element_id"
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "element_id",
+                    "plugin_id",
+                    "label"
+                ]
+            },
+            "minItems": 1
+        }
+    },
+    "required": [
+        "model_id",
+        "label",
+        "events",
+        "actions"
+    ]
+}
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json
new file mode 100644
index 0000000000000000000000000000000000000000..4280d51fa7ef3351a4d8cacbd0ac61b70b1eeb10
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 0__1.json	
@@ -0,0 +1,285 @@
+{
+    "model_id": "eca_test_0001",
+    "label": "Cross references",
+    "version": null,
+    "description": "Two different node types are referring each other. If one node gets saved with a reference to another node, the other node gets automatically updated to link back to the first node.",
+    "events": [
+        {
+            "element_id": "Event_011cx7s",
+            "plugin_id": "content_entity:insert",
+            "label": "Insert node",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1rlgsjy",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Event_1cfd8ek",
+            "plugin_id": "content_entity:update",
+            "label": "Update node",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1rlgsjy",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_1cxcwjm",
+                    "condition": ""
+                }
+            ]
+        }
+    ],
+    "conditions": [
+        {
+            "element_id": "Flow_0iztkfs",
+            "plugin_id": "eca_entity_type_bundle",
+            "configuration": {
+                "negate": false,
+                "type": "node type_1",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_1jqykgu",
+            "plugin_id": "eca_entity_type_bundle",
+            "configuration": {
+                "negate": false,
+                "type": "node type_2",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0i81v8o",
+            "plugin_id": "eca_entity_field_value",
+            "configuration": {
+                "case": false,
+                "expected_value": "[entity:nid]",
+                "field_name": "field_other_node",
+                "operator": "equal",
+                "type": "value",
+                "negate": true,
+                "entity": "refentity"
+            }
+        },
+        {
+            "element_id": "Flow_1tgic5x",
+            "plugin_id": "eca_entity_field_value_empty",
+            "configuration": {
+                "field_name": "field_other_node",
+                "negate": true,
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0rgzuve",
+            "plugin_id": "eca_entity_field_value_empty",
+            "configuration": {
+                "negate": false,
+                "field_name": "field_other_node",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0c3s897",
+            "plugin_id": "eca_entity_field_value_empty",
+            "configuration": {
+                "field_name": "field_other_node",
+                "negate": true,
+                "entity": "originalentity"
+            }
+        }
+    ],
+    "gateways": [
+        {
+            "gateway_id": "Gateway_1xl2rvc",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_0k6im8f",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_1oj601y",
+                    "condition": ""
+                }
+            ]
+        }
+    ],
+    "actions": [
+        {
+            "element_id": "Activity_1rlgsjy",
+            "plugin_id": "eca_token_load_entity",
+            "label": "Load original entity",
+            "configuration": {
+                "token_name": "originalentity",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": true,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_0r1gs9s",
+                    "condition": "Flow_0iztkfs"
+                },
+                {
+                    "element_id": "Activity_0r1gs9s",
+                    "condition": "Flow_1jqykgu"
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_0h8b7vh",
+            "plugin_id": "eca_token_load_entity_ref",
+            "label": "Load referenced node",
+            "configuration": {
+                "field_name_entity_ref": "field_other_node",
+                "token_name": "refentity",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "element_id": "Gateway_1xl2rvc",
+                    "condition": "Flow_0i81v8o"
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_0k6im8f",
+            "plugin_id": "eca_set_field_value",
+            "label": "Set Cross Ref",
+            "configuration": {
+                "field_name": "field_other_node",
+                "field_value": "[entity:nid]",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": true,
+                "object": "refentity"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_0r1gs9s",
+            "plugin_id": "eca_void_and_condition",
+            "label": "void",
+            "configuration": [],
+            "successors": [
+                {
+                    "element_id": "Activity_0h8b7vh",
+                    "condition": "Flow_1tgic5x"
+                },
+                {
+                    "element_id": "Activity_1ch3wrr",
+                    "condition": "Flow_0rgzuve"
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_1oj601y",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "Node [entity:title] references [refentity:title]",
+                "replace_tokens": true
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1cxcwjm",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "Node [entity:title] got updated",
+                "replace_tokens": true
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1w7m4sk",
+            "plugin_id": "eca_token_load_entity_ref",
+            "label": "Load referenced node",
+            "configuration": {
+                "field_name_entity_ref": "field_other_node",
+                "token_name": "originalentityref",
+                "from": "current",
+                "entity_type": "_none",
+                "entity_id": "",
+                "revision_id": "",
+                "properties": "",
+                "langcode": "_interface",
+                "latest_revision": false,
+                "unchanged": false,
+                "object": "originalentity"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1bfoheo",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_077d2t8",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_1ch3wrr",
+            "plugin_id": "eca_void_and_condition",
+            "label": "void",
+            "configuration": [],
+            "successors": [
+                {
+                    "element_id": "Activity_1w7m4sk",
+                    "condition": "Flow_0c3s897"
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_1bfoheo",
+            "plugin_id": "eca_set_field_value",
+            "label": "Empty Cross Ref",
+            "configuration": {
+                "field_name": "field_other_node",
+                "field_value": "",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": true,
+                "object": "originalentityref"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_077d2t8",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "message": "The title of the referenced node is [originalentity:field_other_node:entity:title].",
+                "replace_tokens": true
+            },
+            "successors": []
+        }
+    ]
+}
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json
new file mode 100644
index 0000000000000000000000000000000000000000..8ba24aa99180d1dad3c5e155b8d03d6bc4030ec9
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 1__1.json	
@@ -0,0 +1,180 @@
+{
+    "model_id": "eca_test_0002",
+    "label": "Entity Events Part 1",
+    "version": null,
+    "description": "Triggers custom events in the same model and in another model, see also \"Entity Events Part 2\"",
+    "events": [
+        {
+            "element_id": "Event_0wm7ta0",
+            "plugin_id": "content_entity:presave",
+            "label": "Pre-save",
+            "configuration": {
+                "type": "node _all"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1do22d1",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Event_0sr0xl6",
+            "plugin_id": "content_entity:custom",
+            "label": "C1",
+            "configuration": {
+                "event_id": "C1"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1sh3bdl",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Event_1l6ov1l",
+            "plugin_id": "user:set_user",
+            "label": "Set current user",
+            "configuration": [],
+            "successors": [
+                {
+                    "element_id": "Activity_1p5hvp4",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Event_0n1zpul",
+            "plugin_id": "eca_base:eca_custom",
+            "label": "Cplain",
+            "configuration": {
+                "event_id": ""
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1gguvde",
+                    "condition": ""
+                }
+            ]
+        }
+    ],
+    "conditions": [],
+    "gateways": [],
+    "actions": [
+        {
+            "element_id": "Activity_1do22d1",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message 0: [entity:title]"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_03j3ob6",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_1k70gka",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_150pgta",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_00ca469",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_03j3ob6",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C1",
+            "configuration": {
+                "event_id": "C1",
+                "tokens": "",
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1k70gka",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C2",
+            "configuration": {
+                "event_id": "C2",
+                "tokens": "",
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_150pgta",
+            "plugin_id": "eca_token_load_user_current",
+            "label": "Load current user",
+            "configuration": {
+                "token_name": "user"
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_1acmymx",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_1acmymx",
+            "plugin_id": "eca_trigger_content_entity_custom_event",
+            "label": "Trigger C3",
+            "configuration": {
+                "event_id": "C3",
+                "tokens": "",
+                "object": "user"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1sh3bdl",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message 1: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1p5hvp4",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message set current user: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1gguvde",
+            "plugin_id": "action_message_action",
+            "label": "Msg",
+            "configuration": {
+                "replace_tokens": false,
+                "message": "Message without event: [entity:title]"
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_00ca469",
+            "plugin_id": "eca_trigger_custom_event",
+            "label": "Trigger Cplain",
+            "configuration": {
+                "event_id": "Cplain",
+                "tokens": ""
+            },
+            "successors": []
+        }
+    ]
+}
diff --git a/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json
new file mode 100644
index 0000000000000000000000000000000000000000..e05ea7788a882750611eb61c3ba7fd89ad59ea1c
--- /dev/null
+++ b/modules/agents/tests/src/Kernel/__snapshots__/ModelMapperTest__testMappingFromEntity with data set 2__1.json	
@@ -0,0 +1,307 @@
+{
+    "model_id": "eca_test_0009",
+    "label": "Set field values",
+    "version": null,
+    "description": "Set single and multi value fields with values, testing different variations.",
+    "events": [
+        {
+            "element_id": "Event_056l2f4",
+            "plugin_id": "content_entity:presave",
+            "label": "Presave Node",
+            "configuration": {
+                "type": "node type_set_field_value"
+            },
+            "successors": [
+                {
+                    "element_id": "Gateway_113xj72",
+                    "condition": "Flow_0j7r2le"
+                },
+                {
+                    "element_id": "Gateway_0nagg07",
+                    "condition": "Flow_1eoahw0"
+                },
+                {
+                    "element_id": "Gateway_1tbbhie",
+                    "condition": "Flow_0e3yjwm"
+                },
+                {
+                    "element_id": "Gateway_1byzhmc",
+                    "condition": "Flow_0wind58"
+                },
+                {
+                    "element_id": "Gateway_0aowp4i",
+                    "condition": "Flow_0iwzr0t"
+                }
+            ]
+        }
+    ],
+    "conditions": [
+        {
+            "element_id": "Flow_0j7r2le",
+            "plugin_id": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Append",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_1eoahw0",
+            "plugin_id": "eca_entity_is_new",
+            "configuration": {
+                "negate": false,
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0e3yjwm",
+            "plugin_id": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Drop First",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0wind58",
+            "plugin_id": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Reset",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        },
+        {
+            "element_id": "Flow_0iwzr0t",
+            "plugin_id": "eca_entity_field_value",
+            "configuration": {
+                "negate": false,
+                "case": false,
+                "expected_value": "Drop last",
+                "field_name": "title",
+                "operator": "contains",
+                "type": "value",
+                "entity": ""
+            }
+        }
+    ],
+    "gateways": [
+        {
+            "gateway_id": "Gateway_113xj72",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_0na1ecf",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_1on1kw2",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "gateway_id": "Gateway_1tbbhie",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_03beihz",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "gateway_id": "Gateway_0nagg07",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_1agtxee",
+                    "condition": ""
+                },
+                {
+                    "element_id": "Activity_13qlvkq",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "gateway_id": "Gateway_1byzhmc",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_0yt6yuv",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "gateway_id": "Gateway_0aowp4i",
+            "type": 0,
+            "successors": [
+                {
+                    "element_id": "Activity_036vgtd",
+                    "condition": ""
+                }
+            ]
+        }
+    ],
+    "actions": [
+        {
+            "element_id": "Activity_1agtxee",
+            "plugin_id": "eca_set_field_value",
+            "label": "Set text line",
+            "configuration": {
+                "field_name": "field_text_line",
+                "field_value": "Title is [entity:title].",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_13qlvkq",
+            "plugin_id": "eca_set_field_value",
+            "label": "Set lines 1",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 1",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_0na1ecf",
+            "plugin_id": "eca_set_field_value",
+            "label": "Overwrite text line",
+            "configuration": {
+                "field_name": "field_text_line",
+                "field_value": "The updated text line content.",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_1on1kw2",
+            "plugin_id": "eca_set_field_value",
+            "label": "Append line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Second line",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_0aa91q1",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_0aa91q1",
+            "plugin_id": "eca_set_field_value",
+            "label": "Append another line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 3",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": [
+                {
+                    "element_id": "Activity_0zcaglk",
+                    "condition": ""
+                }
+            ]
+        },
+        {
+            "element_id": "Activity_0zcaglk",
+            "plugin_id": "eca_set_field_value",
+            "label": "Append another line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 4",
+                "method": "append:not_full",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_03beihz",
+            "plugin_id": "eca_set_field_value",
+            "label": "Append line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Line 4",
+                "method": "append:drop_first",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_0yt6yuv",
+            "plugin_id": "eca_set_field_value",
+            "label": "Reset lines",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "This is one line.",
+                "method": "set:clear",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        },
+        {
+            "element_id": "Activity_036vgtd",
+            "plugin_id": "eca_set_field_value",
+            "label": "Prepend line",
+            "configuration": {
+                "field_name": "field_text_lines",
+                "field_value": "Inserted line",
+                "method": "prepend:drop_last",
+                "strip_tags": false,
+                "trim": false,
+                "save_entity": false,
+                "object": ""
+            },
+            "successors": []
+        }
+    ]
+}