diff --git a/demoentityimporter/README.md b/demoentityimporter/README.md
new file mode 100644
index 0000000..654784d
--- /dev/null
+++ b/demoentityimporter/README.md
@@ -0,0 +1,66 @@
+# Demo: register a custom entity importer
+
+This module shows how a PrestaShop 9.2+ module plugs its own entity importer
+into the core import engine (Advanced Parameters > Import) — and actually
+imports something: each CSV row becomes a `DemoNote` Doctrine entity.
+
+## How it works
+
+1. The importer class extends
+ `PrestaShop\PrestaShop\Core\Import\Engine\EntityImporter\AbstractEntityImporter`
+ ([src/Importer/DemoNoteImporter.php](src/Importer/DemoNoteImporter.php)),
+ which provides the cursor-resumable batch loop, the phase-id guard and
+ the default unit count (implementing `EntityImporterInterface` directly
+ also works for full control). It declares two phases — a pausing
+ `validation` phase (note required, max 255 characters) and a `database`
+ phase that persists one `DemoNote` per row (create, or update when the
+ mapped `id` column matches an existing note).
+2. The service is registered with `autoconfigure: true`
+ ([config/services.yml](config/services.yml)). The core registers the
+ `core.import.entity_importer` tag for every autoconfigured service
+ implementing the interface — no manual tag needed.
+3. The core `EntityImporterRegistry` collects every tagged importer: the
+ import page entity dropdown, the mapping screen field list and the batch
+ execution all discover the module importer automatically.
+4. The importer reuses the core engine services instead of re-implementing
+ file handling: `ResumableFileReaderInterface` (cursor-resumable reads of
+ the normalized working file — header/skip rows are already stripped at
+ normalization) and `RowMapper` (column-to-field mapping).
+5. The `DemoNote` entity ([src/Entity/DemoNote.php](src/Entity/DemoNote.php))
+ lives in `src/Entity`, which the core maps automatically for active
+ modules — no Doctrine configuration needed. Its table is created at
+ module install ([demoentityimporter.php](demoentityimporter.php)).
+
+## Verify
+
+After installing the module:
+
+```bash
+php bin/console debug:container --tag=core.import.entity_importer
+```
+
+should list `DemoNoteImporter` next to the core importers.
+
+A sample import file is provided in [sample/demo_notes.csv](sample/demo_notes.csv):
+
+```csv
+id;note
+;First imported note
+;Second imported note
+```
+
+(the empty `id` column means "create"; put an existing note id there to
+update it instead).
+
+## Install
+
+```bash
+cd modules/demoentityimporter
+composer dumpautoload
+```
+
+then install the module (BO module manager or `php bin/console prestashop:module install demoentityimporter`).
+
+## Requirements
+
+- PrestaShop 9.2.0 or newer (import engine introduced by [PrestaShop/PrestaShop#41907](https://github.com/PrestaShop/PrestaShop/issues/41907)).
diff --git a/demoentityimporter/composer.json b/demoentityimporter/composer.json
new file mode 100644
index 0000000..7ca27ea
--- /dev/null
+++ b/demoentityimporter/composer.json
@@ -0,0 +1,19 @@
+{
+ "name": "prestashop/demoentityimporter",
+ "description": "PrestaShop example module: register a custom entity importer into the core import engine",
+ "license": "AFL-3.0",
+ "authors": [
+ {
+ "name": "PrestaShop Core team"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "PrestaShop\\Module\\DemoEntityImporter\\": "src/"
+ }
+ },
+ "config": {
+ "prepend-autoloader": false
+ },
+ "type": "prestashop-module"
+}
diff --git a/demoentityimporter/config.xml b/demoentityimporter/config.xml
new file mode 100644
index 0000000..5d723ea
--- /dev/null
+++ b/demoentityimporter/config.xml
@@ -0,0 +1,11 @@
+
+
+ demoentityimporter
+
+
+
+
+
+ 0
+ 1
+
\ No newline at end of file
diff --git a/demoentityimporter/config/services.yml b/demoentityimporter/config/services.yml
new file mode 100644
index 0000000..d8ab2d0
--- /dev/null
+++ b/demoentityimporter/config/services.yml
@@ -0,0 +1,10 @@
+services:
+ _defaults:
+ public: true
+ autowire: true
+ # autoconfigure is what gets the service automatically tagged as an
+ # entity importer (the core registers the tag for every autoconfigured
+ # service implementing EntityImporterInterface)
+ autoconfigure: true
+
+ PrestaShop\Module\DemoEntityImporter\Importer\DemoNoteImporter: ~
diff --git a/demoentityimporter/demoentityimporter.php b/demoentityimporter/demoentityimporter.php
new file mode 100644
index 0000000..2dcc93c
--- /dev/null
+++ b/demoentityimporter/demoentityimporter.php
@@ -0,0 +1,72 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
+ */
+
+declare(strict_types=1);
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+class DemoEntityImporter extends Module
+{
+ public function __construct()
+ {
+ $this->name = 'demoentityimporter';
+ $this->author = 'PrestaShop';
+ $this->version = '1.0.0';
+ $this->ps_versions_compliancy = ['min' => '9.2.0', 'max' => '9.99.99'];
+
+ parent::__construct();
+
+ $this->displayName = $this->trans('Demo - register a custom entity importer', [], 'Modules.Demoentityimporter.Admin');
+ $this->description = $this->trans('Shows how a module registers its own importer into the core import engine and persists a Doctrine entity.', [], 'Modules.Demoentityimporter.Admin');
+ }
+
+ public function install(): bool
+ {
+ return parent::install() && $this->installDatabase();
+ }
+
+ public function uninstall(): bool
+ {
+ return parent::uninstall() && $this->uninstallDatabase();
+ }
+
+ /**
+ * Creates the table backing the DemoNote Doctrine entity
+ * (src/Entity/DemoNote.php).
+ */
+ private function installDatabase(): bool
+ {
+ return Db::getInstance()->execute('
+ CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'demo_note` (
+ `id_demo_note` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `note` VARCHAR(255) NOT NULL,
+ `date_add` DATETIME NOT NULL,
+ PRIMARY KEY (`id_demo_note`)
+ ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;
+ ');
+ }
+
+ private function uninstallDatabase(): bool
+ {
+ return Db::getInstance()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'demo_note`');
+ }
+}
diff --git a/demoentityimporter/sample/demo_notes.csv b/demoentityimporter/sample/demo_notes.csv
new file mode 100644
index 0000000..1dfab1e
--- /dev/null
+++ b/demoentityimporter/sample/demo_notes.csv
@@ -0,0 +1,3 @@
+id;note
+;First imported note
+;Second imported note
diff --git a/demoentityimporter/src/Entity/DemoNote.php b/demoentityimporter/src/Entity/DemoNote.php
new file mode 100644
index 0000000..00066e1
--- /dev/null
+++ b/demoentityimporter/src/Entity/DemoNote.php
@@ -0,0 +1,90 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\DemoEntityImporter\Entity;
+
+use DateTime;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * The entity the demo importer persists. Module entities living in
+ * src/Entity are mapped automatically by the core (annotation driver,
+ * active modules only) — no extra Doctrine configuration is needed.
+ *
+ * @ORM\Table()
+ * @ORM\Entity()
+ */
+class DemoNote
+{
+ /**
+ * @var int
+ *
+ * @ORM\Id
+ *
+ * @ORM\Column(name="id_demo_note", type="integer")
+ *
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(name="note", type="string", length=255)
+ */
+ private $note;
+
+ /**
+ * @var DateTime
+ *
+ * @ORM\Column(name="date_add", type="datetime")
+ */
+ private $dateAdd;
+
+ public function __construct(string $note)
+ {
+ $this->note = $note;
+ $this->dateAdd = new DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getNote(): string
+ {
+ return $this->note;
+ }
+
+ public function setNote(string $note): self
+ {
+ $this->note = $note;
+
+ return $this;
+ }
+
+ public function getDateAdd(): DateTime
+ {
+ return $this->dateAdd;
+ }
+}
diff --git a/demoentityimporter/src/Importer/DemoNoteImporter.php b/demoentityimporter/src/Importer/DemoNoteImporter.php
new file mode 100644
index 0000000..cb6512e
--- /dev/null
+++ b/demoentityimporter/src/Importer/DemoNoteImporter.php
@@ -0,0 +1,163 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\DemoEntityImporter\Importer;
+
+use Doctrine\ORM\EntityManagerInterface;
+use PrestaShop\Module\DemoEntityImporter\Entity\DemoNote;
+use PrestaShop\PrestaShop\Core\Import\Engine\EntityImporter\AbstractEntityImporter;
+use PrestaShop\PrestaShop\Core\Import\Engine\EntityImporter\RowMapper;
+use PrestaShop\PrestaShop\Core\Import\Engine\ImportMessage;
+use PrestaShop\PrestaShop\Core\Import\Engine\ImportPhaseDefinition;
+use PrestaShop\PrestaShop\Core\Import\Engine\ImportRunContext;
+use PrestaShop\PrestaShop\Core\Import\Engine\PhaseBatchResult;
+use PrestaShop\PrestaShop\Core\Import\EntityField\EntityField;
+use PrestaShop\PrestaShop\Core\Import\EntityField\EntityFieldCollection;
+use PrestaShop\PrestaShop\Core\Import\EntityField\EntityFieldCollectionInterface;
+use PrestaShop\PrestaShop\Core\Import\File\ResumableFileReaderInterface;
+use Throwable;
+
+/**
+ * A complete module importer: it validates the file in a pausing
+ * 'validation' phase, then persists one DemoNote Doctrine entity per row in
+ * the 'database' phase (create, or update when an id column matches an
+ * existing note).
+ *
+ * Because the service is autoconfigured (see config/services.yml), the core
+ * automatically tags it and it appears in the EntityImporterRegistry next to
+ * the core importers: the import page entity dropdown, the mapping screen
+ * and the batch execution all pick it up with no extra wiring.
+ *
+ * The heavy lifting comes from the core:
+ * - extending AbstractEntityImporter provides the cursor-resumable batch
+ * loop (iterateBatch), the phase-id guard and the default unit count;
+ * - ResumableFileReaderInterface and RowMapper are core services injected
+ * as-is.
+ *
+ * See PrestaShop\PrestaShop\Core\Import\Engine\EntityImporter\ProductImporter
+ * in the core for the full-scale reference implementation.
+ */
+class DemoNoteImporter extends AbstractEntityImporter
+{
+ public const ENTITY_TYPE = 'demo_note';
+
+ protected const NOTE_MAX_LENGTH = 255;
+
+ public function __construct(
+ ResumableFileReaderInterface $fileReader,
+ RowMapper $rowMapper,
+ protected readonly EntityManagerInterface $entityManager,
+ ) {
+ parent::__construct($fileReader, $rowMapper);
+ }
+
+ public function getEntityType(): string
+ {
+ return self::ENTITY_TYPE;
+ }
+
+ public function getLabel(): string
+ {
+ return 'Demo notes';
+ }
+
+ public function getFields(): EntityFieldCollectionInterface
+ {
+ return EntityFieldCollection::createFromArray([
+ new EntityField('id', 'ID'),
+ new EntityField('note', 'Note', '', true),
+ ]);
+ }
+
+ public function getPhases(): array
+ {
+ return [
+ new ImportPhaseDefinition(ImportPhaseDefinition::PHASE_VALIDATION, 'Validating notes', true),
+ new ImportPhaseDefinition(ImportPhaseDefinition::PHASE_DATABASE, 'Importing notes'),
+ ];
+ }
+
+ public function processPhaseBatch(string $phaseId, ImportRunContext $context, int $limit): PhaseBatchResult
+ {
+ $this->assertKnownPhase($phaseId);
+
+ if (ImportPhaseDefinition::PHASE_VALIDATION === $phaseId) {
+ return $this->iterateBatch($context, $limit, function (array $row, int $rowIndex): array {
+ $messages = [];
+ $note = $row['note'] ?? '';
+ if ('' === $note) {
+ $messages[] = $this->message(ImportMessage::SEVERITY_ERROR, ImportPhaseDefinition::PHASE_VALIDATION, 'The note text is required.', $rowIndex);
+ } elseif (mb_strlen($note) > self::NOTE_MAX_LENGTH) {
+ $messages[] = $this->message(ImportMessage::SEVERITY_ERROR, ImportPhaseDefinition::PHASE_VALIDATION, sprintf('The note exceeds %d characters.', self::NOTE_MAX_LENGTH), $rowIndex);
+ }
+
+ // an error marks the row as skipped for the later phases
+ return ['messages' => $messages, 'skipped' => $this->containsError($messages)];
+ });
+ }
+
+ return $this->iterateBatch($context, $limit, function (array $row, int $rowIndex) use ($context): array {
+ if ($context->isRowSkipped($rowIndex)) {
+ return ['messages' => [], 'skipped' => false];
+ }
+
+ try {
+ $this->importRow($row);
+ } catch (Throwable $e) {
+ // a failing row must fail THIS ROW only, never the batch
+ return [
+ 'messages' => [$this->message(ImportMessage::SEVERITY_ERROR, ImportPhaseDefinition::PHASE_DATABASE, sprintf('The note could not be saved: %s', $e->getMessage()), $rowIndex)],
+ 'skipped' => true,
+ ];
+ }
+
+ return ['messages' => [], 'skipped' => false];
+ });
+ }
+
+ /**
+ * @param array $row mapped row values
+ */
+ protected function importRow(array $row): void
+ {
+ $note = null;
+ $id = $row['id'] ?? '';
+ if (ctype_digit($id)) {
+ $note = $this->entityManager->find(DemoNote::class, (int) $id);
+ }
+
+ if (null !== $note) {
+ $note->setNote($row['note']);
+ } else {
+ $note = new DemoNote($row['note']);
+ $this->entityManager->persist($note);
+ }
+
+ $this->entityManager->flush();
+ }
+
+ protected function message(string $severity, string $phase, string $text, int $rowIndex): ImportMessage
+ {
+ return new ImportMessage($severity, $phase, $text, $rowIndex, 'note');
+ }
+
+}