<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Spryker Documentation</title>
        <description>Spryker documentation center.</description>
        <link>https://docs.spryker.com/</link>
        <atom:link href="https://docs.spryker.com/feed.xml" rel="self" type="application/rss+xml"/>
        <lastBuildDate>Mon, 02 Nov 2026 12:08:51 +0000</lastBuildDate>
        <generator>Jekyll v4.2.2</generator>
        
        
        <item>
            <title>Security release notes 202610.0</title>
            <description>This document describes the security-related issues that have been recently resolved.

For additional support with this content, [contact our support](https://support.spryker.com/). If you found a new security vulnerability, contact us at [security@spryker.com](mailto:security@spryker.com).

## Stripe payment page can be accessed by unauthenticated users

Due to missing access controls, the Stripe payment page could be accessed by unauthenticated users. This could lead to the exposure of sensitive information, including customers&apos; names, phone numbers, and addresses. Also, a visitor who was not logged in could cancel any order through the payment cancel page. Exploiting either issue requires knowledge of the randomly generated order reference.

### Affected modules

- `spryker-shop/payment-page`: &lt; 1.9.0
- `spryker-eco/stripe`: &lt; 1.1.0

### Fix the vulnerability

After the fix, only the following visitors can open the Stripe payment page:

- A logged-in customer who owns the order.
- A guest whose current session placed the order.
- A visitor who returns from Stripe with the `payment_intent_client_secret` query parameter of the order&apos;s PaymentIntent.

A visitor who is not logged in and does not meet these conditions is redirected to the login page. A logged-in customer who does not own the order is redirected to the home page. Also, a visitor who is not logged in can cancel an order on the payment cancel page only when the current session placed the order.

1. Update the affected packages:

```bash
composer update spryker-eco/stripe:&quot;^1.1.0&quot; spryker-shop/payment-page:&quot;^1.9.0&quot;
composer show spryker-eco/stripe spryker-shop/payment-page # Verify the versions
```

2. The update adds the `StripePaymentDetailsRequest` transfer and the `/stripe/gateway/get-payment-details-by-request` backend gateway route. Generate the transfers and rebuild the backend gateway route cache. Without the rebuilt route cache, the payment page fails with a 404 error from the backend gateway.

```bash
console transfer:generate
console router:cache:warm-up:backend-gateway
```

3. To return customers to the payment page after they log in, make sure `RedirectUriCustomerRedirectStrategyPlugin` is registered. If your project uses `LastVisitedPageCustomerRedirectStrategyPlugin`, register `RedirectUriCustomerRedirectStrategyPlugin` before it.

**src/Pyz/Yves/CustomerPage/CustomerPageDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Yves\CustomerPage;

use SprykerShop\Yves\CustomerPage\CustomerPageDependencyProvider as SprykerShopCustomerPageDependencyProvider;
use SprykerShop\Yves\CustomerPage\Plugin\CustomerPage\RedirectUriCustomerRedirectStrategyPlugin;

class CustomerPageDependencyProvider extends SprykerShopCustomerPageDependencyProvider
{
    /**
     * @return array&lt;\SprykerShop\Yves\CustomerPageExtension\Dependency\Plugin\CustomerRedirectStrategyPluginInterface&gt;
     */
    protected function getAfterLoginCustomerRedirectPlugins(): array
    {
        return [
            new RedirectUriCustomerRedirectStrategyPlugin(),
        ];
    }
}
```

4. If your project overrides `SprykerEco\Yves\Stripe\Controller\PaymentController` or `SprykerShop\Yves\PaymentPage\Controller\PaymentCancelController`, apply the same access checks to your overrides.

5. If your project calls `StripeFacade::getPaymentDetails()` or `StripeClient::getPaymentDetails()`, replace the calls with `getPaymentDetailsByRequest()`. The old methods are deprecated and now return an unsuccessful response unless the passed customer reference belongs to the order owner.

{% info_block warningBox &quot;Verification&quot; %}

1. Place an order with Stripe as a logged-in customer.
2. In a private browser window, open `/stripe/payment?orderReference={order_reference}`. Make sure you are redirected to the login page and the URL contains the `redirectUri` parameter.
3. Log in as the customer who placed the order. Make sure you are redirected to the payment page.
4. Log in as a different customer and open the same URL. Make sure you are redirected to the home page.

{% endinfo_block %}

## Session fixation on login

A session ID is the value stored in the session cookie that identifies a user to the application. Storefront, Back Office and Merchant Portal applications did not regenerate the session ID when a user logged in, including after completing the multi-factor authentication (MFA) step.

Because the session ID remained unchanged, the application was vulnerable to session fixation attacks. An attacker who could set a known session ID in a victim&apos;s browser before the victim logged in could gain authenticated access once the victim completed login. The attacker could then act on behalf of the victim without knowing their credentials.

### Affected modules

- `spryker/multi-factor-auth`: &lt; 2.10.0
- `spryker/multi-factor-auth-merchant-portal`: &lt; 2.5.0
- `spryker/security`: &lt; 1.12.0
- `spryker-shop/session-agent-validation`: &lt; 1.3.0

### Fix the vulnerability

After the fix, the application generates a new session ID when a user logs in, so a session ID issued before login cannot be used to access the authenticated session.

Update the affected packages:

```bash
composer update spryker/multi-factor-auth:&quot;^2.10.0&quot; spryker/multi-factor-auth-merchant-portal:&quot;^2.5.0&quot; spryker/security:&quot;^1.12.0&quot; spryker-shop/session-agent-validation:&quot;^1.3.0&quot;
```

{% info_block warningBox &quot;Verification&quot; %}

1. On the Storefront, open the login page and, in your browser&apos;s developer tools, note the value of the session cookie.
2. Log in. If MFA is enabled, complete the MFA step.
3. Make sure the value of the session cookie differs from the value you noted before login.
4. Repeat the steps in the Back Office and Merchant Portal.

{% endinfo_block %}

## Vulnerability in the @angular/router package of the Merchant Portal

The Merchant Portal frontend is built with Angular. The `@angular/router` package, which handles navigation between Merchant Portal pages, has a known security vulnerability in versions 22.0.0 to 22.1.x. This issue affects only projects that use the Merchant Portal.

### Affected packages

- `@angular/router`: &gt;= 22.0.0 &lt; 22.2.0

### Check if your project is affected

To check if your project uses a vulnerable version of `@angular/router`, run an npm audit:

```bash
docker/sdk cli &quot;npm audit&quot;
```

If the output reports `@angular/router`, apply the fix.

### Fix the vulnerability

To fix the vulnerability, update the Spryker packages that define the Angular dependencies of the Merchant Portal, and then update all `@angular/*` packages to 22.2.3. All `@angular/*` packages must have the same version to work together.

1. Update the Spryker packages:

```bash
composer update spryker/falcon-ui:&quot;^0.1.5&quot; spryker-shop/date-time-configurator-page-example:&quot;^0.7.5&quot; spryker/zed-ui:&quot;^4.5.1&quot;
```

2. Update the npm packages:

```bash
docker/sdk cli &quot;npm update --workspaces&quot;
```

{% info_block warningBox &quot;Verification&quot; %}

1. Make sure that all `@angular/*` packages are on version 22.2.3 and that each package is installed only once:

```bash
npm ls | grep -E &apos;@angular/[a-z-]+@&apos;
```

2. Make sure that the packages are hosted to the project root: the `node_modules/@angular/core` directory exists, and the following command returns `0`:

```bash
grep -c &apos;zed-ui/node_modules/@angular/&apos; package-lock.json
```

3. Make sure that the audit no longer reports `@angular/router`:

```bash
docker/sdk cli &quot;npm audit | grep -A3 &apos;@angular/router&apos;&quot;
```

4. Build the Merchant Portal and make sure the build succeeds:

```bash
docker/sdk cli &quot;npm run mp:build&quot;
```

{% endinfo_block %}

## Rate limiting protection for email change on the StoreFront

The StoreFront email-change function had no rate limit, which allowed an authenticated user to send unlimited confirmation emails to any address. Confirmation emails are now rate-limited per customer and per recipient address.

### Affected modules

- `spryker/customer`: &lt; 7.88.0
- `spryker/customer-data-change-request`: &lt; 1.4.0
- `spryker/redis`: &lt; 2.15.0
- `spryker/security-blocker`: &lt; 1.4.0
- `spryker-shop/customer-page`: &lt; 2.88.1
- `spryker/storage-redis`: &lt; 1.10.1

### Fix the vulnerability

When a customer changes their email address on the Storefront profile page, the application sends a verification email to the new address. After the update, the application limits email change requests per customer and per target email address. By default, a customer can request an email change up to 3 times within 15 minutes, and a target email address can receive up to 3 verification emails within 15 minutes, regardless of which customer requests them. When the limit is reached, the email address is not changed, no verification email is sent, and the customer sees an error message on the profile page.

1. Update the affected packages:

```bash
composer update spryker/customer:&quot;^7.88.0&quot; spryker/customer-data-change-request:&quot;^1.4.0&quot; spryker/redis:&quot;^2.15.0&quot; spryker/security-blocker:&quot;^1.4.0&quot; spryker-shop/customer-page:&quot;^2.88.1&quot; spryker/storage-redis:&quot;^1.10.1&quot;
```

2. Generate the transfers:

```bash
console transfer:generate
```

3. To show the error message when the limit is reached, add the following translations to `data/import/common/common/glossary.csv`:

```csv
customer.change_customer_email_limit_exceeded,&quot;You reached the limit of email change requests. Please try again later.&quot;,en_US
customer.change_customer_email_limit_exceeded,&quot;Sie haben das Limit für Anfragen zur Änderung der E-Mail-Adresse erreicht. Bitte versuchen Sie es später erneut.&quot;,de_DE
```

4. Import the glossary:

```bash
console data:import:glossary
```

5. Optional: To change the default limit, override the `EMAIL_CHANGE_REQUEST_LIMIT` and `EMAIL_CHANGE_REQUEST_LIMIT_TTL_SECONDS` constants on the project level. To disable the limit, set `EMAIL_CHANGE_REQUEST_LIMIT` to `0`.

**src/Pyz/Zed/CustomerDataChangeRequest/CustomerDataChangeRequestConfig.php**

```php
&lt;?php

namespace Pyz\Zed\CustomerDataChangeRequest;

use Spryker\Zed\CustomerDataChangeRequest\CustomerDataChangeRequestConfig as SprykerCustomerDataChangeRequestConfig;

class CustomerDataChangeRequestConfig extends SprykerCustomerDataChangeRequestConfig
{
    protected const int EMAIL_CHANGE_REQUEST_LIMIT = 3;

    protected const int EMAIL_CHANGE_REQUEST_LIMIT_TTL_SECONDS = 900;
}
```

{% info_block warningBox &quot;Verification&quot; %}

1. On the Storefront, log in as a customer and go to the profile page.
2. Change the email address and save the form. Repeat 3 times. Make sure each submission sends a verification email to the new address.
3. Submit the form with a new email address for the 4th time within 15 minutes. Make sure an error message about the reached limit is displayed and no verification email is sent.

{% endinfo_block %}

## Rate limiting protection for password change

On the Storefront, Back Office, and Merchant Portal, a logged-in user can change their password by entering their current password, a new password, and a confirmation of the new password. The password change forms did not limit the number of attempts. As a result, anyone who gained access to a logged-in session without knowing the account password, for example through a stolen session cookie or an unlocked device, could guess the current password an unlimited number of times. After guessing it, they could set a new password and lock the account owner out. The number of password change attempts is now limited per user.

### Affected modules

- `spryker/customer`: &lt; 7.89.0
- `spryker/customers-rest-api`: &lt; 1.36.0
- `spryker/user`: &lt; 3.36.0
- `spryker/user-merchant-portal-gui`: &lt; 4.5.0

### Fix the vulnerability

After the update, when a user reaches the limit of password change attempts, the password is not changed and the user sees an error message.

1. Update the affected packages:

```bash
composer update spryker/customer:&quot;^7.89.0&quot; spryker/customers-rest-api:&quot;^1.36.0&quot; spryker/user:&quot;^3.36.0&quot; spryker/user-merchant-portal-gui:&quot;^4.5.0&quot;
```

2. To show the error message on the Storefront when the limit is reached, add the following translations to `data/import/common/common/glossary.csv`:

```csv
customer.password.change_limit_exceeded,&quot;You reached the limit of password change attempts. Please try again later.&quot;,en_US
customer.password.change_limit_exceeded,&quot;Sie haben das Limit für Versuche zur Änderung des Passworts erreicht. Bitte versuchen Sie es später erneut.&quot;,de_DE
```

3. Import the glossary:

```bash
console data:import:glossary
```

## SQL injection in the content item list of the Back Office

When you add a content item to a CMS page or block in the Back Office, the CMS editor shows a list of available content items. The endpoint that returns this list did not escape a request parameter before using it in a database query. As a result, an authenticated Back Office user could run arbitrary SQL queries against the application database.

### Affected modules

- `spryker/content-gui`: &lt; 3.2.2

### Fix the vulnerability

Update the affected package:

```bash
composer update spryker/content-gui:&quot;^3.2.2&quot;
```

{% info_block warningBox &quot;Verification&quot; %}

1. In the Back Office, open a CMS page or block for editing.
2. In the content editor, add a content item, for example a banner. Make sure the list of content items is displayed and you can select an item.

{% endinfo_block %}

## Company user takeover through the Add company user form

On the Storefront, a company administrator can add users to their company on the **Add company user** page. The form submission was not validated correctly, so a company administrator could overwrite a company user that belongs to another company. As a result, the affected user was moved to the attacker&apos;s company and lost access to their own company, and data linked to that user, such as shared carts and shopping lists, became available in the attacker&apos;s company.

### Affected modules

- `spryker/company-user`: &lt; 2.25.0
- `spryker-shop/company-page`: &lt; 2.39.0

### Fix the vulnerability

Update the affected packages:

```bash
composer update spryker/company-user:&quot;^2.25.0&quot; spryker-shop/company-page:&quot;^2.39.0&quot;
```

## Cross-merchant price disclosure in the Merchant Portal

In the Merchant Portal, the price tables on the abstract and concrete product edit pages load their data from separate endpoints. These endpoints did not check whether the requested product belongs to the merchant of the current user. As a result, an authenticated merchant user could read the prices of another merchant&apos;s products, including prices negotiated with specific business customers through merchant relations.

### Affected modules

- `spryker/product-merchant-portal-gui`: &lt; 5.6.0

### Fix the vulnerability

Update the affected package:

```bash
composer update spryker/product-merchant-portal-gui:&quot;^5.6.0&quot;
```

## Missing CSRF token validation on login

A cross-site request forgery (CSRF) token is a random value that a form submits to prove that the request comes from the application&apos;s own page. The login forms of the Storefront, Back Office, Merchant Portal, and Agent Assist rendered a CSRF token but did not validate it. As a result, a malicious website could silently log a victim in to an account controlled by the attacker. The attacker could then see the actions that the victim performed in that account, for example, the addresses or payment details they entered.

### Affected modules

- `spryker/agent-security-merchant-portal-gui`: &lt; 2.5.0
- `spryker/security`: &lt; 1.13.0
- `spryker/security-gui`: &lt; 2.11.0
- `spryker/security-merchant-portal-gui`: &lt; 4.8.0
- `spryker-shop/agent-page`: &lt; 1.28.0
- `spryker-shop/customer-page`: &lt; 2.89.0

### Fix the vulnerability

Update the affected packages:

```bash
composer update spryker/agent-security-merchant-portal-gui:&quot;^2.5.0&quot; spryker/security:&quot;^1.13.0&quot; spryker/security-gui:&quot;^2.11.0&quot; spryker/security-merchant-portal-gui:&quot;^4.8.0&quot; spryker-shop/agent-page:&quot;^1.28.0&quot; spryker-shop/customer-page:&quot;^2.89.0&quot;
```

{% info_block warningBox &quot;Login templates overridden on the project level&quot; %}

After the update, the application rejects every login request without a valid CSRF token. If your project overrides a login form template, make sure the template renders the `_token` field of the form. Otherwise, no user can log in.

{% endinfo_block %}
</description>
            <pubDate>Mon, 02 Nov 2026 12:08:28 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/security-releases/security-release-notes-202610.0.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/security-releases/security-release-notes-202610.0.html</guid>
            
            
        </item>
        
        <item>
            <title>Upgrade the ProductExperienceManagement module</title>
            <description>## Upgrade from version 4.0.* to version 5.0.0

*Estimated migration time: 1 hour*

Up to version 4.0.*, the `ProductExperienceManagement` module contained both the product CSV import and the engine that runs any CSV import: the `spy_import_job*` tables, the Back Office *Import &amp; Export* pages, the `import:job:run` console command, and the plugin interfaces for import steps. From version 5.0.0, the engine lives in the new `spryker/data-exchange` module and the plugin interfaces in `spryker/data-exchange-extension`; together they form the [Data Exchange Jobs](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html) feature. `ProductExperienceManagement` keeps only the product plugin: `ProductCsvImportSchemaPlugin`, the product import and export steps, and `ProductCsvExportPlaceholderProvider`.

The database tables, the transfer objects, the console command name, and the Back Office menu are unchanged. The following table lists what moved and what the project has to adjust.

&lt;div class=&quot;width-100&quot;&gt;

| BEFORE 5.0.0 | FROM 5.0.0 |
| --- | --- |
| `SprykerFeature\Zed\ProductExperienceManagement\Business\Dependency\Plugin\*` interfaces | `Spryker\Zed\DataExchangeExtension\Dependency\Plugin\*` interfaces |
| `SprykerFeature\Zed\ProductExperienceManagement\Business\Export\PlaceholderProvider\ExportPlaceholderProviderInterface` | `Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ColumnPlaceholderProviderInterface`, returned by `ImportSchemaPluginInterface::getPlaceholderProvider()` |
| `ProductExperienceManagementFacadeInterface` methods `getImportJobCollection()`, `createImportJobCollection()`, `createImportJobRunCollection()`, `getImportJobRunCollection()`, `getImportJobRunErrorCollection()`, `exportData()`, `processNextPendingRun()` | The same methods on `Spryker\Zed\DataExchange\Business\DataExchangeFacadeInterface` |
| `SprykerFeature\Zed\ProductExperienceManagement\Communication\Console\ImportJobRunConsole` | `Spryker\Zed\DataExchange\Communication\Console\ImportJobRunConsole` |
| `ProductExperienceManagementDependencyProvider::PLUGINS_IMPORT_SCHEMA`, `PLUGINS_IMPORT_PRE_PROCESSOR`, `PLUGINS_IMPORT_POST_PROCESSOR` | `DataExchangeDependencyProvider::getImportSchemaPlugins()`, `getImportPreProcessorPlugins()`, `getImportPostProcessorPlugins()` |
| Engine settings in `ProductExperienceManagementConfig`: `IMPORT_ERROR_DISPLAY_THRESHOLD`, `getImportCsvBatchSize()`, `getExportCsvBatchSize()`, `getImportMaxFileSizeString()`, `getAllowedMimeTypesWithExtensions()`, `getImportFileSystemName()`, `getExportFileSystemName()` | The same settings in `Spryker\Zed\DataExchange\DataExchangeConfig` |
| `Orm\Zed\ProductExperienceManagement\Persistence\SpyImportJob*` entities and queries | `Orm\Zed\DataExchange\Persistence\SpyImportJob*` |
| `ImportJob*`, `ImportStep*`, and `ImportPublishEvent` transfers defined in `ProductExperienceManagement` | The same transfers defined in `DataExchange` and `DataExchangeExtension` |
| Back Office URLs `/product-experience-management/{job,run,export,template}` | `/data-exchange/{job,run,export,template}` |
| Filesystem entries `product-experience-management-imports` and `product-experience-management-exports` | `data-exchange-imports` and `data-exchange-exports` |

&lt;/div&gt;

To upgrade, follow the steps below.

### 1) Update the module

```bash
composer require spryker/data-exchange:&quot;^0.1.0&quot; spryker-feature/product-experience-management:&quot;^5.0.0&quot; --update-with-dependencies
```

The module requires `spryker/data-exchange-extension` only; the engine `spryker/data-exchange` is required by the project, as in the command above.

### 2) Register the product schema plugin

Before 5.0.0, the product plugin was registered in the module&apos;s own dependency provider. The engine now reads its plugins from the `DataExchange` dependency provider:

**src/Pyz/Zed/DataExchange/DataExchangeDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Zed\DataExchange;

use Spryker\Zed\DataExchange\DataExchangeDependencyProvider as SprykerDataExchangeDependencyProvider;
use SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin\ProductCsvImportSchemaPlugin;

class DataExchangeDependencyProvider extends SprykerDataExchangeDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportSchemaPluginInterface&gt;
     */
    protected function getImportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ExportSchemaPluginInterface&gt;
     */
    protected function getExportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }
}
```

The engine resolves the import, the template download, and the job creation through `getImportSchemaPlugins()`, and the export with data through `getExportSchemaPlugins()`; the product plugin implements both sides, so it is registered in both stacks. If your project overrides `getImportSchemaPlugins()`, `getImportPreProcessorPlugins()`, or `getImportPostProcessorPlugins()` in `Pyz\Zed\ProductExperienceManagement\ProductExperienceManagementDependencyProvider`, move the overrides to the class above and remove them from the old provider.

### 3) Rename the filesystem entries

Rename the two entries under `FileSystemConstants::FILESYSTEM_SERVICE` in every configuration file where they are defined, usually `config/Shared/config_default.php` and the environment-specific files:

| OLD NAME | NEW NAME |
| --- | --- |
| `product-experience-management-imports` | `data-exchange-imports` |
| `product-experience-management-exports` | `data-exchange-exports` |

Keep the adapter, bucket, and path settings of each entry. If the entries read the bucket from environment variables, rename `SPRYKER_S3_PEM_IMPORT_BUCKET` and `SPRYKER_S3_PEM_EXPORT_BUCKET` to `SPRYKER_S3_DATA_EXCHANGE_IMPORT_BUCKET` and `SPRYKER_S3_DATA_EXCHANGE_EXPORT_BUCKET` in the configuration and in every environment that sets them; the `key` and `secret` settings are not used by the IAM adapter and can be dropped. The engine reads the files of existing import runs from the renamed entries, so the files remain accessible as long as the new entries point at the same storage. If you prefer to keep the old names, override `getImportFileSystemName()` and `getExportFileSystemName()` in `Pyz\Zed\DataExchange\DataExchangeConfig` instead.

### 4) Update the navigation

Point the **Import** and **Export** pages of the `import-export` node at the new bundle:

**config/Zed/navigation.xml**

```xml
&lt;import&gt;
    &lt;bundle&gt;data-exchange&lt;/bundle&gt;
    &lt;controller&gt;job&lt;/controller&gt;
    &lt;action&gt;index&lt;/action&gt;
&lt;/import&gt;
&lt;export&gt;
    &lt;bundle&gt;data-exchange&lt;/bundle&gt;
    &lt;controller&gt;export&lt;/controller&gt;
    &lt;action&gt;index&lt;/action&gt;
&lt;/export&gt;
```

### 5) Update the console command registration

Replace the import of the console command in `src/Pyz/Zed/Console/ConsoleDependencyProvider.php`:

```php
use Spryker\Zed\DataExchange\Communication\Console\ImportJobRunConsole;
```

The command name `import:job:run` and the scheduler entry `import-job-run` are unchanged.

### 6) Move engine configuration overrides

If your project overrides engine settings in `Pyz\Zed\ProductExperienceManagement\ProductExperienceManagementConfig`, move them to `Pyz\Zed\DataExchange\DataExchangeConfig`. This applies to `IMPORT_ERROR_DISPLAY_THRESHOLD`, `getImportCsvBatchSize()`, `getExportCsvBatchSize()`, `getImportMaxFileSizeString()`, `getAllowedMimeTypesWithExtensions()`, `getImportFileSystemName()`, and `getExportFileSystemName()`. The product-specific settings `getProductConcreteStatuses()` and `getProductConcreteStatusActive()` stay in `ProductExperienceManagementConfig`.

### 7) Update custom import steps and schema plugins

If your project contains its own import steps or schema plugins, update them as follows:

- Replace `implements \SprykerFeature\Zed\ProductExperienceManagement\Business\Dependency\Plugin\ImportStepInterface` with `implements \Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportStepInterface`. Steps that extend `AbstractProductCsvImportStep` need no change; its `createImportStepError()` and `createImportPublishEvent()` helpers are unchanged. In `ProductCsvImportSearchStep`, `ProductCsvImportPriceStep`, and `ProductCsvImportPriceStoreStep`, the nullable helpers `resolveAbstractSku()` and `resolveConcreteSku()` were renamed to `findAbstractSku()` and `findConcreteSku()`; update subclasses that call them.
- Replace the imports of `ImportSchemaPluginInterface`, `ExportSchemaPluginInterface`, `ExportStepInterface`, `ExportDataProviderInterface`, `ImportPreProcessorPluginInterface`, and `ImportPostProcessorPluginInterface` with the interfaces of the same names in `Spryker\Zed\DataExchangeExtension\Dependency\Plugin`.
- Implement the new `getPlaceholderProvider()` method in every schema plugin. `getType()`, `getSchema()`, and `getPlaceholderProvider()` now live in `SchemaPluginInterface`, the parent of `ImportSchemaPluginInterface` and `ExportSchemaPluginInterface`; a plugin that implements the export side must also be registered in `getExportSchemaPlugins()`. It returns a `ColumnPlaceholderProviderInterface` with the values of the placeholders used in the plugin&apos;s schema. Before 5.0.0, the engine used the product placeholder provider for every job type.

### 8) Update Back Office ACL rules

Back Office ACL rules and group rules are defined by bundle, controller, and action. Rules that grant the Import &amp; Export pages through the `product-experience-management` bundle no longer match the moved pages, so the affected users lose access after the upgrade. Add rules for the `data-exchange` bundle with the controllers `job`, `run`, `export`, and `template` to the same groups, or widen the existing rules, before the upgrade is deployed.

### 9) Update facade and entity references

- Replace calls to the removed `ProductExperienceManagementFacadeInterface` methods listed in the table above with the same methods on `DataExchangeFacadeInterface`, available through `$container-&gt;getLocator()-&gt;dataExchange()-&gt;facade()`.
- Replace references to `Orm\Zed\ProductExperienceManagement\Persistence\SpyImportJob*` classes with `Orm\Zed\DataExchange\Persistence\SpyImportJob*`.
- Replace links to `/product-experience-management/job`, `/product-experience-management/run`, `/product-experience-management/export`, and `/product-experience-management/template` with the `/data-exchange/` counterparts, including in end-to-end tests.

### 9) Regenerate the code and the caches

```bash
console transfer:generate
console propel:install
console navigation:build-cache
console router:cache:warm-up:backoffice
console translator:generate-cache
console cache:empty-all
```

Build the Back Office frontend so that the export page&apos;s JavaScript is published from its new location:

```bash
console frontend:zed:build
```

{% info_block warningBox &quot;Verification&quot; %}

Log in to the Back Office and verify the following:

- **Import &amp; Export&amp;nbsp;&gt; Import** lists the existing import jobs, and **Create Import Job** offers **products-csv-import** as a type.
- Uploading a CSV file for an existing job creates a run, and `console import:job:run` processes it.
- **Import &amp; Export&amp;nbsp;&gt; Export** downloads a CSV file for an existing job.

{% endinfo_block %}
</description>
            <pubDate>Fri, 30 Oct 2026 18:23:35 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/upgrade-the-productexperiencemanagement-module.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/upgrade-the-productexperiencemanagement-module.html</guid>
            
            
        </item>
        
        <item>
            <title>Install the Data Exchange Jobs feature</title>
            <description>This document describes how to install the Data Exchange Jobs feature: the engine that stores import jobs and runs, processes uploaded files, and provides the Back Office **Import &amp; Export** pages. The engine imports and exports no data on its own; a data type is added by registering a schema plugin. To install the product data type, see [Install the Back Office Product CSV Import and Export feature](/docs/pbc/all/product-experience-management/latest/install/install-the-back-office-product-csv-import-and-export-feature.html). To add your own data type, see [Add a custom data type to Data Exchange Jobs](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html).

## Prerequisites

Install the required features:

| NAME | VERSION | REQUIRED |
| --- | --- | --- |
| Spryker Core | {{page.release_tag}} | Required |
| File System | {{page.release_tag}} | Required |

## Install feature core

### 1) Install the required modules

```bash
composer require spryker/data-exchange:&quot;{{page.release_tag}}&quot; spryker/data-exchange-extension:&quot;{{page.release_tag}}&quot; --update-with-dependencies
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following modules have been installed:

| MODULE | EXPECTED DIRECTORY |
| --- | --- |
| DataExchange | vendor/spryker/data-exchange |
| DataExchangeExtension | vendor/spryker/data-exchange-extension |

{% endinfo_block %}

### 2) Set up configuration

#### Filesystems

The engine reads uploaded files from and writes exported files to two named filesystem entries: `data-exchange-imports` and `data-exchange-exports`. The names are returned by `getImportFileSystemName()` and `getExportFileSystemName()` of `DataExchangeConfig`; override the methods on the project level if you need different names.

##### Production (S3)

**config/Shared/config_default.php**

To enable an IAM role configuration, use `IamAws3v3FilesystemBuilderPlugin`:

```php
use Spryker\Service\FlysystemAws3v3FileSystem\Plugin\Flysystem\IamAws3v3FilesystemBuilderPlugin;
use Spryker\Shared\FileSystem\FileSystemConstants;

$config[FileSystemConstants::FILESYSTEM_SERVICE] = [
    // ... existing entries ...
    &apos;data-exchange-imports&apos; =&gt; [
        &apos;sprykerAdapterClass&apos; =&gt; IamAws3v3FilesystemBuilderPlugin::class,
        &apos;bucket&apos; =&gt; getenv(&apos;SPRYKER_S3_DATA_EXCHANGE_IMPORT_BUCKET&apos;) ?: &apos;&apos;,
        &apos;region&apos; =&gt; getenv(&apos;AWS_REGION&apos;) ?: &apos;&apos;,
        &apos;path&apos; =&gt; &apos;data-exchange-imports/&apos;,
    ],
    &apos;data-exchange-exports&apos; =&gt; [
        &apos;sprykerAdapterClass&apos; =&gt; IamAws3v3FilesystemBuilderPlugin::class,
        &apos;bucket&apos; =&gt; getenv(&apos;SPRYKER_S3_DATA_EXCHANGE_EXPORT_BUCKET&apos;) ?: &apos;&apos;,
        &apos;region&apos; =&gt; getenv(&apos;AWS_REGION&apos;) ?: &apos;&apos;,
        &apos;path&apos; =&gt; &apos;data-exchange-exports/&apos;,
    ],
];
```

{% info_block warningBox &quot;IAM authentication required&quot; %}

These filesystems authenticate through the IAM role attached to the workload, which resolves AWS credentials automatically. Access keys (`key` and `secret`) are not supported. For more details, see [AWS S3 filesystem builder plugins](/docs/dg/dev/backend-development/data-manipulation/data-ingestion/structural-preparations/flysystem.html#aws-s3-filesystem-builder-plugins).

{% endinfo_block %}

##### Development (local filesystem)

**config/Shared/config_default-docker.dev.php**

```php
use Spryker\Service\FlysystemLocalFileSystem\Plugin\Flysystem\LocalFilesystemBuilderPlugin;
use Spryker\Shared\FileSystem\FileSystemConstants;

$config[FileSystemConstants::FILESYSTEM_SERVICE][&apos;data-exchange-imports&apos;] = [
    &apos;sprykerAdapterClass&apos; =&gt; LocalFilesystemBuilderPlugin::class,
    &apos;root&apos; =&gt; &apos;/data&apos;,
    &apos;path&apos; =&gt; &apos;/data/data-exchange-imports&apos;,
];

$config[FileSystemConstants::FILESYSTEM_SERVICE][&apos;data-exchange-exports&apos;] = [
    &apos;sprykerAdapterClass&apos; =&gt; LocalFilesystemBuilderPlugin::class,
    &apos;root&apos; =&gt; &apos;/data&apos;,
    &apos;path&apos; =&gt; &apos;/data/data-exchange-exports&apos;,
];
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the filesystem directories exist and are writable by the application. For local development, verify that the `/data/data-exchange-imports` and `/data/data-exchange-exports` directories are created inside the Docker container.

{% endinfo_block %}

#### Engine limits and error display

The following settings are defined in `Spryker\Zed\DataExchange\DataExchangeConfig`:

| SETTING | DEFAULT | DESCRIPTION |
| --- | --- | --- |
| `IMPORT_CSV_BATCH_SIZE` | `1000` | Number of CSV rows handed to the import steps at once. |
| `EXPORT_CSV_BATCH_SIZE` | `1000` | Number of rows the export data provider returns at once. |
| `IMPORT_MAX_FILE_SIZE_MB` | `50` | Maximum size of an uploaded CSV file in megabytes. |
| `IMPORT_ERROR_DISPLAY_THRESHOLD` | `10` | Maximum number of import errors displayed inline on the import job run detail page. When a run produces more errors than this threshold, the errors are offered as a downloadable summary instead. |
| `getAllowedMimeTypesWithExtensions()` | `text/csv`, `text/plain` | MIME types accepted by the upload form. |

To change the defaults, extend the module configuration on the project level:

**src/Pyz/Zed/DataExchange/DataExchangeConfig.php**

```php
&lt;?php

namespace Pyz\Zed\DataExchange;

use Spryker\Zed\DataExchange\DataExchangeConfig as SprykerDataExchangeConfig;

class DataExchangeConfig extends SprykerDataExchangeConfig
{
    protected const int IMPORT_ERROR_DISPLAY_THRESHOLD = 20;
}
```

### 3) Set up the database schema and transfer objects

Apply database changes and generate entity and transfer changes:

```bash
console propel:install
console transfer:generate
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure that the following changes have been applied by checking your database:

| DATABASE ENTITY | TYPE | EVENT |
| --- | --- | --- |
| spy_import_job | table | created |
| spy_import_job_run | table | created |
| spy_import_job_run_error | table | created |

{% endinfo_block %}

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following changes in transfer objects have been applied:

| TRANSFER | TYPE | EVENT | PATH |
| --- | --- | --- | --- |
| ImportJob | class | created | src/Generated/Shared/Transfer/ImportJobTransfer |
| ImportJobRun | class | created | src/Generated/Shared/Transfer/ImportJobRunTransfer |
| ImportJobRunFileInfo | class | created | src/Generated/Shared/Transfer/ImportJobRunFileInfoTransfer |
| ImportJobRunError | class | created | src/Generated/Shared/Transfer/ImportJobRunErrorTransfer |
| ImportJobCollection | class | created | src/Generated/Shared/Transfer/ImportJobCollectionTransfer |
| ImportJobRunCollection | class | created | src/Generated/Shared/Transfer/ImportJobRunCollectionTransfer |
| ImportJobRunErrorCollection | class | created | src/Generated/Shared/Transfer/ImportJobRunErrorCollectionTransfer |
| ImportJobRunCollectionRequest | class | created | src/Generated/Shared/Transfer/ImportJobRunCollectionRequestTransfer |
| ImportJobRunCollectionResponse | class | created | src/Generated/Shared/Transfer/ImportJobRunCollectionResponseTransfer |
| ImportJobCollectionRequest | class | created | src/Generated/Shared/Transfer/ImportJobCollectionRequestTransfer |
| ImportJobCollectionResponse | class | created | src/Generated/Shared/Transfer/ImportJobCollectionResponseTransfer |
| ImportJobCriteria | class | created | src/Generated/Shared/Transfer/ImportJobCriteriaTransfer |
| ImportJobConditions | class | created | src/Generated/Shared/Transfer/ImportJobConditionsTransfer |
| ImportJobRunCriteria | class | created | src/Generated/Shared/Transfer/ImportJobRunCriteriaTransfer |
| ImportJobRunConditions | class | created | src/Generated/Shared/Transfer/ImportJobRunConditionsTransfer |
| ImportJobRunErrorCriteria | class | created | src/Generated/Shared/Transfer/ImportJobRunErrorCriteriaTransfer |
| ImportJobRunErrorConditions | class | created | src/Generated/Shared/Transfer/ImportJobRunErrorConditionsTransfer |
| ImportJobExportResult | class | created | src/Generated/Shared/Transfer/ImportJobExportResultTransfer |
| ImportStepResponse | class | created | src/Generated/Shared/Transfer/ImportStepResponseTransfer |
| ImportStepError | class | created | src/Generated/Shared/Transfer/ImportStepErrorTransfer |
| ImportPublishEvent | class | created | src/Generated/Shared/Transfer/ImportPublishEventTransfer |

{% endinfo_block %}

### 4) Register the schema plugins

The engine learns about job types from the schema plugins registered in the `DataExchange` dependency provider. Register one plugin per data type. The following example registers the product data type of the Product Experience Management capability; see [Add a custom data type to Data Exchange Jobs](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html) for your own types.

&lt;div class=&quot;width-100&quot;&gt;

| PLUGIN | SPECIFICATION | PREREQUISITES | NAMESPACE |
| --- | --- | --- | --- |
| ProductCsvImportSchemaPlugin | Provides the *products-csv-import* job type: its column schema, import steps, export steps, and placeholder values. | Product Experience Management feature | SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin |

&lt;/div&gt;

**src/Pyz/Zed/DataExchange/DataExchangeDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Zed\DataExchange;

use Spryker\Zed\DataExchange\DataExchangeDependencyProvider as SprykerDataExchangeDependencyProvider;
use SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin\ProductCsvImportSchemaPlugin;

class DataExchangeDependencyProvider extends SprykerDataExchangeDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportSchemaPluginInterface&gt;
     */
    protected function getImportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ExportSchemaPluginInterface&gt;
     */
    protected function getExportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }
}
```

`getImportSchemaPlugins()` serves the import, the template download, and the job creation; `getExportSchemaPlugins()` serves the export with data. A type that is not exportable is registered in the import stack only. The same dependency provider offers `getImportPreProcessorPlugins()` and `getImportPostProcessorPlugins()` for plugins that run once before and once after the rows of a run are processed.

{% info_block warningBox &quot;Verification&quot; %}

Log in to the Back Office, go to **Import &amp; Export&amp;nbsp;&gt; Import**, and click **Create Import Job**. Make sure that every registered job type is offered in the **Type** field.

{% endinfo_block %}

### 5) Configure navigation

Add the following entry to the Back Office navigation:

**config/Zed/navigation.xml**

```xml
&lt;import-export&gt;
    &lt;label&gt;Import &amp;amp; Export&lt;/label&gt;
    &lt;title&gt;Import &amp;amp; Export&lt;/title&gt;
    &lt;icon&gt;swap_vert&lt;/icon&gt;
    &lt;pages&gt;
        &lt;import&gt;
            &lt;label&gt;Import&lt;/label&gt;
            &lt;title&gt;Import&lt;/title&gt;
            &lt;bundle&gt;data-exchange&lt;/bundle&gt;
            &lt;controller&gt;job&lt;/controller&gt;
            &lt;action&gt;index&lt;/action&gt;
            &lt;visible&gt;1&lt;/visible&gt;
        &lt;/import&gt;
        &lt;export&gt;
            &lt;label&gt;Export&lt;/label&gt;
            &lt;title&gt;Export&lt;/title&gt;
            &lt;bundle&gt;data-exchange&lt;/bundle&gt;
            &lt;controller&gt;export&lt;/controller&gt;
            &lt;action&gt;index&lt;/action&gt;
            &lt;visible&gt;1&lt;/visible&gt;
        &lt;/export&gt;
    &lt;/pages&gt;
&lt;/import-export&gt;
```

Build the navigation cache:

```bash
console navigation:cache:remove
```

{% info_block warningBox &quot;Verification&quot; %}

Log in to the Back Office and verify that the **Import &amp; Export** section appears in the main navigation with the **Import** and **Export** sub-items.

{% endinfo_block %}

### 6) Set up console

#### Register the console command

Register the `import:job:run` console command that processes pending import job runs:

**src/Pyz/Zed/Console/ConsoleDependencyProvider.php**

```php
use Spryker\Zed\DataExchange\Communication\Console\ImportJobRunConsole;

// In getConsoleCommands():
new ImportJobRunConsole(),
```

{% info_block warningBox &quot;Verification&quot; %}

Execute the following and verify it completes without errors:

```bash
console import:job:run
```

The command prints `Processing next pending import job run...` and `Done.` and exits with code 0. It processes the oldest pending import job run; when no run is pending, nothing is processed.

{% endinfo_block %}

#### Configure the scheduler

Register the command to run on a schedule so that pending runs are picked up automatically:

**src/Pyz/SymfonyScheduler/src/Pyz/Client/SymfonyScheduler/SymfonySchedulerConfig.php**

```php
&apos;import-job-run&apos; =&gt; [
    &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console import:job:run&apos;,
    &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
],
```

Add the entry to the array that `getJobs()` returns. If the other entries in your file prefix the command with the `$logger` variable to redirect the output to the scheduler log, prefix this one in the same way.

{% info_block warningBox &quot;Verification&quot; %}

Verify that the scheduler configuration is loaded:

```bash
console scheduler:setup
```

Check that `import-job-run` appears in the list of scheduled jobs.

{% endinfo_block %}

### 7) Build the Back Office frontend

The **Export** page ships a JavaScript entry point. Build the Back Office frontend to publish it:

```bash
console frontend:zed:build
```

{% info_block warningBox &quot;Verification&quot; %}

Log in to the Back Office, go to **Import &amp; Export&amp;nbsp;&gt; Export**, select a job, and make sure that the download link changes to the selected job.

{% endinfo_block %}
</description>
            <pubDate>Fri, 30 Oct 2026 18:23:35 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html</guid>
            
            
        </item>
        
        <item>
            <title>Install the Back Office Product CSV Import and Export feature</title>
            <description>The Back Office Product CSV Import and Export feature is the product data type of the [Data Exchange Jobs](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html) feature. The engine that stores jobs and runs, processes files, and provides the Back Office **Import &amp; Export** pages is installed separately; this document installs the product plugin on top of it.

## Prerequisites

Install the required features:

| NAME | VERSION | REQUIRED |
| --- | --- | --- |
| Spryker Core | {{page.release_tag}} | Required |
| Data Exchange Jobs | {{page.release_tag}} | Required |
| Product | {{page.release_tag}} | Required |
| Product Image | {{page.release_tag}} | Required |
| Price Product | {{page.release_tag}} | Required |
| Stock | {{page.release_tag}} | Required |
| Tax | {{page.release_tag}} | Required |
| Store | {{page.release_tag}} | Required |
| Locale | {{page.release_tag}} | Required |
| Category | {{page.release_tag}} | Required |
| Merchant | {{page.release_tag}} | Required |
| Product Approval Process | {{page.release_tag}} | Required |
| Shipment Type | {{page.release_tag}} | Required |

For the Data Exchange Jobs feature, see [Install the Data Exchange Jobs feature](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html).

## Install feature core

### 1) Install the required modules

```bash
composer require spryker-feature/product-experience-management:&quot;{{page.release_tag}}&quot; --update-with-dependencies
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following modules have been installed:

| MODULE | EXPECTED DIRECTORY |
| --- | --- |
| ProductExperienceManagement | vendor/spryker-feature/product-experience-management |

{% endinfo_block %}

### 2) Set up configuration

The product plugin reads the *Product Status* column of the imported CSV file and relies on `spryker/product-approval` for the approval statuses of abstract products. The following settings are defined in `SprykerFeature\Zed\ProductExperienceManagement\ProductExperienceManagementConfig`:

| SETTING | DESCRIPTION |
| --- | --- |
| `getProductConcreteStatuses()` | Statuses accepted in the *Product Status* column of concrete product rows. Rows with any other value are skipped and reported as errors. |
| `getProductConcreteStatusActive()` | Status that activates a concrete product. Every other status from `getProductConcreteStatuses()` deactivates it. |

To change the defaults, extend the module configuration on the project level:

**src/Pyz/Zed/ProductExperienceManagement/ProductExperienceManagementConfig.php**

```php
&lt;?php

namespace Pyz\Zed\ProductExperienceManagement;

use SprykerFeature\Zed\ProductExperienceManagement\ProductExperienceManagementConfig as SprykerProductExperienceManagementConfig;

class ProductExperienceManagementConfig extends SprykerProductExperienceManagementConfig
{
    /**
     * @return list&lt;string&gt;
     */
    public function getProductConcreteStatuses(): array
    {
        return [&apos;active&apos;, &apos;inactive&apos;];
    }

    public function getProductConcreteStatusActive(): string
    {
        return &apos;active&apos;;
    }
}
```

The engine settings, such as the batch size, the maximum upload size, and the number of errors shown inline, are defined in `DataExchangeConfig`; see [Install the Data Exchange Jobs feature](/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html#2-set-up-configuration).

### 3) Set up transfer objects

Generate the transfer objects of the product plugin:

```bash
console transfer:generate
```

{% info_block warningBox &quot;Verification&quot; %}

Make sure the following changes in transfer objects have been applied:

| TRANSFER | TYPE | EVENT | PATH |
| --- | --- | --- | --- |
| ImportRowValidationCollection | class | created | src/Generated/Shared/Transfer/ImportRowValidationCollectionTransfer |

{% endinfo_block %}

### 4) Register the product schema plugin

The engine learns about the *products-csv-import* job type from the schema plugin registered in the `DataExchange` dependency provider. Without this registration, the type is not offered when creating an import job.

&lt;div class=&quot;width-100&quot;&gt;

| PLUGIN | SPECIFICATION | PREREQUISITES | NAMESPACE |
| --- | --- | --- | --- |
| ProductCsvImportSchemaPlugin | Provides the *products-csv-import* job type: its column schema, import steps, export steps, and placeholder values. | None | SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin |

&lt;/div&gt;

**src/Pyz/Zed/DataExchange/DataExchangeDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Zed\DataExchange;

use Spryker\Zed\DataExchange\DataExchangeDependencyProvider as SprykerDataExchangeDependencyProvider;
use SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin\ProductCsvImportSchemaPlugin;

class DataExchangeDependencyProvider extends SprykerDataExchangeDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportSchemaPluginInterface&gt;
     */
    protected function getImportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ExportSchemaPluginInterface&gt;
     */
    protected function getExportSchemaPlugins(): array
    {
        return [
            new ProductCsvImportSchemaPlugin(),
        ];
    }
}
```

{% info_block warningBox &quot;Verification&quot; %}

Log in to the Back Office, go to **Import &amp; Export&amp;nbsp;&gt; Import**, and click **Create Import Job**. Make sure that **products-csv-import** is offered in the **Type** field.

Create a job with this type, download its template, and make sure that the header row contains the product columns, for example *Abstract SKU*, *Concrete SKU*, and one *Name (…)* column per locale.

Import a CSV file with a concrete product row and verify the following:

- A row with the status returned by `getProductConcreteStatusActive()` activates the concrete product.
- A row with a status that is not in `getProductConcreteStatuses()` is skipped and reported as an error.

{% endinfo_block %}
</description>
            <pubDate>Fri, 30 Oct 2026 18:13:36 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/install-the-back-office-product-csv-import-and-export-feature.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/install/install-the-back-office-product-csv-import-and-export-feature.html</guid>
            
            
        </item>
        
        <item>
            <title>Data Exchange Jobs feature overview</title>
            <description>&lt;p&gt;The &lt;em&gt;Data Exchange Jobs&lt;/em&gt; feature lets you move data into and out of Spryker in bulk through files. A &lt;em&gt;job&lt;/em&gt; defines what data is exchanged and which columns the file has, a &lt;em&gt;run&lt;/em&gt; processes one uploaded file asynchronously, and every rejected row is recorded with its row number and reason. The feature is provided by the &lt;code&gt;DataExchange&lt;/code&gt; module and is independent of the data it moves: the data types are plugged in by other modules. Products are the first data type, provided by the &lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/product-experience-management.html&quot;&gt;Product Experience Management&lt;/a&gt; capability.&lt;/p&gt;
&lt;p&gt;Back Office users manage jobs, runs, exports, and templates under &lt;strong&gt;Import &amp;amp; Export&lt;/strong&gt;. The engine behind the pages exposes the same operations through its facade, so other interfaces can drive it.&lt;/p&gt;
&lt;h2 id=&quot;terminology&quot;&gt;Terminology&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TERM&lt;/th&gt;
&lt;th&gt;DEFINITION&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Import job&lt;/td&gt;
&lt;td&gt;A reusable definition that specifies the job type (for example, &lt;em&gt;products-csv-import&lt;/em&gt;) and the column schema used to map file headers to system properties. An import job is created once and referenced when uploading files and when exporting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import job run&lt;/td&gt;
&lt;td&gt;A single execution of an import job. Each run is linked to one uploaded CSV file and tracks the processing status, the row counts, and the errors. Runs are processed asynchronously by the &lt;code&gt;import:job:run&lt;/code&gt; console command.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job type&lt;/td&gt;
&lt;td&gt;The key that links an import job to the schema plugin that handles its data, for example &lt;em&gt;products-csv-import&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;The list of columns of the file, stored on the import job when the job is created. Each column maps the header in the file (for example, &lt;code&gt;Name ({locale})&lt;/code&gt;) to a system property name (for example, &lt;code&gt;name.{locale}&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placeholder&lt;/td&gt;
&lt;td&gt;A variable part of a column header in curly braces, such as &lt;code&gt;{locale}&lt;/code&gt; or &lt;code&gt;{store}&lt;/code&gt;. A placeholder provider supplies the values, and the engine expands the column once per value when it builds the template and the export file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema plugin&lt;/td&gt;
&lt;td&gt;The plugin that provides the schema, the import steps, the placeholder provider, and optionally the export steps for one job type. A data type costs one schema plugin, one import step per concern, and one line in the project’s dependency provider.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import step&lt;/td&gt;
&lt;td&gt;A unit of work in the import pipeline. Each step handles one concern of the data type (for example, the product itself, its prices, or its images) and receives every batch of rows in order. Steps validate and persist rows and report errors and publish events.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export step&lt;/td&gt;
&lt;td&gt;The export counterpart of an import step. Each export step fills the columns of its concern for a batch of rows.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;modules&quot;&gt;Modules&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MODULE&lt;/th&gt;
&lt;th&gt;RESPONSIBILITY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spryker/data-exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The engine: import jobs, job runs, run errors, the Back Office &lt;strong&gt;Import &amp;amp; Export&lt;/strong&gt; pages, the &lt;code&gt;import:job:run&lt;/code&gt; console command, batch processing, and file storage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spryker/data-exchange-extension&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The plugin interfaces a data type implements to plug into the engine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spryker-feature/product-experience-management&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The product data type: &lt;code&gt;ProductCsvImportSchemaPlugin&lt;/code&gt; with the &lt;em&gt;products-csv-import&lt;/em&gt; schema and the product import and export steps. See &lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/back-office-product-csv-import-and-export-feature-overview.html&quot;&gt;Back Office product CSV import and export feature overview&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;import-workflow&quot;&gt;Import workflow&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A Back Office user creates an import job that defines the job type. The engine copies the schema of the matching schema plugin to the job.&lt;/li&gt;
&lt;li&gt;The user uploads a CSV file against the import job. The engine stores the file and creates an import job run with the &lt;em&gt;pending&lt;/em&gt; status.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;import:job:run&lt;/code&gt; console command, scheduled to run every minute, picks up the oldest pending run and marks it as &lt;em&gt;processing&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The engine reads the file in batches of 1000 rows. The headers are mapped to system property names using the job’s schema; columns that are not part of the schema are ignored, and rows whose number of columns differs from the header row are recorded as errors.&lt;/li&gt;
&lt;li&gt;Each batch passes through the import steps of the schema plugin in order. A step validates and persists its part of the row and reports an error for every row it rejects.&lt;/li&gt;
&lt;li&gt;After the last row, the engine triggers the publish events reported by the steps, so that Storefront data is updated once per entity. The rows themselves are committed as each step persists them; there is no transaction around the run.&lt;/li&gt;
&lt;li&gt;The run is marked as &lt;em&gt;done&lt;/em&gt; with the number of processed, successful, and failed rows. The run is marked as &lt;em&gt;failed&lt;/em&gt; when no row was processed, when every row failed, or when a step threw an exception; the exception message is recorded as a run error. In that case the rows of the batches processed before the failure stay in the database, and the publish events collected so far are not triggered.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A row is counted as failed when at least one step rejected it. The other steps still process the row, so a data type should validate before it persists.&lt;/p&gt;
&lt;h2 id=&quot;limits-and-performance&quot;&gt;Limits and performance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;import:job:run&lt;/code&gt; command processes one run per call. With the one-minute schedule, one consumer processes about one run per minute; a pending run waits until the runs queued before it are done.&lt;/li&gt;
&lt;li&gt;The import reads the file in batches; the batch size is &lt;code&gt;getImportCsvBatchSize()&lt;/code&gt; in &lt;code&gt;DataExchangeConfig&lt;/code&gt; (default 1000 rows). The maximum upload size is &lt;code&gt;getImportMaxFileSizeString()&lt;/code&gt; (default 50 MB).&lt;/li&gt;
&lt;li&gt;The export runs synchronously inside the HTTP request of the &lt;strong&gt;Export&lt;/strong&gt; page and pages through the data with &lt;code&gt;getExportCsvBatchSize()&lt;/code&gt; (default 1000 rows). A very large export can hit the web server timeout or the PHP memory limit; split such data sets by job or raise the limits for the Back Office.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;export-workflow&quot;&gt;Export workflow&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A Back Office user selects an import job on the &lt;strong&gt;Export&lt;/strong&gt; page and triggers the export.&lt;/li&gt;
&lt;li&gt;The engine resolves the schema of the job and expands the placeholders in the column headers with the values of the schema plugin’s placeholder provider.&lt;/li&gt;
&lt;li&gt;The export data provider of the schema plugin returns the rows in batches of 1000, and the export steps fill the columns.&lt;/li&gt;
&lt;li&gt;The resulting CSV file is stored in the export filesystem and streamed to the user.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A job type is exportable only if a plugin implementing &lt;code&gt;ExportSchemaPluginInterface&lt;/code&gt; is registered for it in &lt;code&gt;getExportSchemaPlugins()&lt;/code&gt;. The &lt;strong&gt;Export&lt;/strong&gt; page lists every import job; exporting a job whose type has no export plugin is rejected with an error.&lt;/p&gt;
&lt;h2 id=&quot;template-download&quot;&gt;Template download&lt;/h2&gt;
&lt;p&gt;For every import job, users can download a CSV template that contains the header row with placeholders expanded and no data rows. The template shows the expected file format before the import file is prepared.&lt;/p&gt;
&lt;h2 id=&quot;error-handling&quot;&gt;Error handling&lt;/h2&gt;
&lt;p&gt;When a run rejects a row, the row is skipped and an error is recorded with the row number and a message. The run detail page shows the processed, successful, and failed row counts. Up to 10 errors are displayed inline; larger error sets are offered as a downloadable CSV file. The threshold is defined by &lt;code&gt;getImportErrorDisplayThreshold()&lt;/code&gt; in &lt;code&gt;DataExchangeConfig&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;related-developer-documents&quot;&gt;Related Developer documents&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INSTALLATION GUIDES&lt;/th&gt;
&lt;th&gt;TUTORIALS AND HOWTOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Install the Data Exchange Jobs feature&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html&quot;&gt;Add a custom data type to Data Exchange Jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
            <pubDate>Fri, 30 Oct 2026 18:13:36 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Add a custom data type to Data Exchange Jobs</title>
            <description>&lt;p&gt;The &lt;em&gt;Data Exchange Jobs&lt;/em&gt; feature is driven by the &lt;code&gt;DataExchange&lt;/code&gt; module, a type-agnostic engine that does not know what data it moves. It stores import jobs and runs, reads uploaded CSV files in batches, records per-row errors, and writes export files. The data itself is handled by &lt;em&gt;schema plugins&lt;/em&gt;: one plugin per job type that tells the engine which columns the file has and which steps process each batch of rows. The product CSV workflow is one such plugin, &lt;code&gt;ProductCsvImportSchemaPlugin&lt;/code&gt; of the &lt;code&gt;ProductExperienceManagement&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;This document shows how to add a second job type, using a customer import as the example. A new type costs one schema plugin class, one import step class per concern, and one line in the project’s dependency provider. Neither the engine nor the product module changes.&lt;/p&gt;
&lt;p&gt;For an overview of the engine, see &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html&quot;&gt;Data Exchange Jobs feature overview&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Install the &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Data Exchange Jobs feature&lt;/a&gt;. It installs the &lt;code&gt;DataExchange&lt;/code&gt; and &lt;code&gt;DataExchangeExtension&lt;/code&gt; modules the plugin interfaces come from.&lt;/p&gt;
&lt;h2 id=&quot;define-the-schema-plugin&quot;&gt;1. Define the schema plugin&lt;/h2&gt;
&lt;p&gt;The schema plugin implements &lt;code&gt;ImportSchemaPluginInterface&lt;/code&gt; from the &lt;code&gt;DataExchangeExtension&lt;/code&gt; module and answers four questions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;METHOD&lt;/th&gt;
&lt;th&gt;RETURNS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getType()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The unique job type. Back Office users pick it in the &lt;strong&gt;Type&lt;/strong&gt; field when creating an import job, and the engine uses it to find the plugin for a run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getSchema()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The columns of the file: for every column, the header as it appears in the file (&lt;code&gt;property_name_in_file&lt;/code&gt;) and the system property name the import steps work with (&lt;code&gt;system_property_name&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getImportSteps()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The import steps in processing order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getPlaceholderProvider()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The values of the placeholders used in the column headers; see &lt;a href=&quot;#placeholders-in-column-headers&quot;&gt;Placeholders in column headers&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The engine stores the schema on the import job when the job is created, so changing &lt;code&gt;getSchema()&lt;/code&gt; affects jobs created afterwards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/Pyz/Zed/CustomerDataExchange/Communication/Plugin/DataExchange/CustomerCsvImportSchemaPlugin.php&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pyz\Zed\CustomerDataExchange\Communication\Plugin\DataExchange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pyz\Zed\CustomerDataExchange\Business\ImportStep\CustomerCsvImportStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pyz\Zed\CustomerDataExchange\Business\PlaceholderProvider\EmptyPlaceholderProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ColumnPlaceholderProviderInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportSchemaPluginInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\Kernel\Communication\AbstractPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvImportSchemaPlugin&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractPlugin&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportSchemaPluginInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TYPE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;customers-csv-import&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TYPE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;int, array&amp;lt;string, mixed&amp;gt;&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getSchema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;property_name_in_file&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;system_property_name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;property_name_in_file&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;First Name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;system_property_name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;first_name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;property_name_in_file&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Last Name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;system_property_name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;last_name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;property_name_in_file&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Salutation&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;system_property_name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;salutation&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportStepInterface&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getImportSteps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvImportStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getPlaceholderProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ColumnPlaceholderProviderInterface&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EmptyPlaceholderProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The example keeps the step and the placeholder provider in a dedicated project module, &lt;code&gt;CustomerDataExchange&lt;/code&gt;, whose factory provides the &lt;code&gt;Customer&lt;/code&gt; facade. Place them in the module that owns the data you import.&lt;/p&gt;
&lt;h3 id=&quot;placeholders-in-column-headers&quot;&gt;Placeholders in column headers&lt;/h3&gt;
&lt;p&gt;A column header can contain placeholders in curly braces, for example &lt;code&gt;Name ({locale})&lt;/code&gt; mapped to &lt;code&gt;name.{locale}&lt;/code&gt;. The engine expands such a column once per placeholder value when it builds the CSV template and the export file, and it matches the expanded headers back to the system property names when it reads an uploaded file. The values come from the placeholder provider of the schema plugin:&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pyz\Zed\CustomerDataExchange\Business\PlaceholderProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ColumnPlaceholderProviderInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalePlaceholderProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ColumnPlaceholderProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;string, array&amp;lt;string&amp;gt;&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getPlaceholderValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;&apos;locale&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;de_DE&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;en_US&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;A placeholder with no values removes every column that uses it. If the schema has no placeholders, as in the customer example, return an empty array:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/Pyz/Zed/CustomerDataExchange/Business/PlaceholderProvider/EmptyPlaceholderProvider.php&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pyz\Zed\CustomerDataExchange\Business\PlaceholderProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ColumnPlaceholderProviderInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EmptyPlaceholderProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ColumnPlaceholderProviderInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;string, array&amp;lt;string&amp;gt;&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getPlaceholderValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;implement-the-import-step&quot;&gt;2. Implement the import step&lt;/h2&gt;
&lt;p&gt;An import step implements &lt;code&gt;ImportStepInterface&lt;/code&gt; and receives every batch of rows of a run, in the order the steps are returned by &lt;code&gt;getImportSteps()&lt;/code&gt;. The engine ships no base class, so your module depends on &lt;code&gt;spryker/data-exchange-extension&lt;/code&gt; only; the step builds the two transfers it reports itself:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TRANSFER&lt;/th&gt;
&lt;th&gt;PURPOSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImportStepErrorTransfer&lt;/code&gt; (&lt;code&gt;csvRowNumber&lt;/code&gt;, &lt;code&gt;errorMessage&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Reports that a row failed. The engine stores the error with the row number and counts the row as failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImportPublishEventTransfer&lt;/code&gt; (&lt;code&gt;eventName&lt;/code&gt;, &lt;code&gt;entityId&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Asks the engine to trigger a publish event for an entity after the whole file has been processed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;$rows&lt;/code&gt; argument is indexed by the row number in the file, and each row is indexed by the system property names of the schema. The &lt;code&gt;$propertyNamesInFile&lt;/code&gt; argument maps every system property name back to the header in the file, so that error messages can name the column the user sees.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/Pyz/Zed/CustomerDataExchange/Business/ImportStep/CustomerCsvImportStep.php&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pyz\Zed\CustomerDataExchange\Business\ImportStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Generated\Shared\Transfer\CustomerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Generated\Shared\Transfer\ImportPublishEventTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Generated\Shared\Transfer\ImportStepErrorTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Generated\Shared\Transfer\ImportStepResponseTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\Customer\Business\CustomerFacadeInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportStepInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvImportStep&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportStepInterface&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;EVENT_CUSTOMER_PUBLISH&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Customer.customer.publish&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__construct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CustomerFacadeInterface&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;cd&quot;&gt;/**
     * @param array&amp;lt;int, array&amp;lt;string, string&amp;gt;&amp;gt; $rows
     * @param array&amp;lt;string, string&amp;gt; $propertyNamesInFile
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;executeBatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$propertyNamesInFile&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImportStepResponseTransfer&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportStepResponseTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setIsSuccessful&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rows&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$rowNumber&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setIsSuccessful&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportStepErrorTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setCsvRowNumber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rowNumber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sprintf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Column &quot;%s&quot; must not be empty.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$propertyNamesInFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)));&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;nv&quot;&gt;$customerResponseTransfer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customerFacade&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addCustomer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setFirstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;first_name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setLastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;last_name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setSalutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;salutation&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerResponseTransfer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getIsSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setIsSuccessful&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerResponseTransfer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$customerErrorTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportStepErrorTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                        &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setCsvRowNumber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$rowNumber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerErrorTransfer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getMessageOrFail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()));&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addPublishEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImportPublishEventTransfer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setEventName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EVENT_CUSTOMER_PUBLISH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setEntityId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$customerResponseTransfer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerTransferOrFail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getIdCustomerOrFail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()));&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Keep the following engine rules in mind when writing a step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Steps run in the order of &lt;code&gt;getImportSteps()&lt;/code&gt;.&lt;/strong&gt; A step that needs entities another step creates, for example the address step after the customer step, must be registered after it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every step sees every batch.&lt;/strong&gt; Split the work by concern, for example one step for the customer and one for its addresses, and let each step read only the columns it needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A row fails as soon as one step reports an error for it&lt;/strong&gt;, but the remaining steps still receive the row. Validate before you persist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Steps are stateless.&lt;/strong&gt; A step must not depend on data from a previous batch or from another step.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entity events are off while rows are written.&lt;/strong&gt; The engine disables the &lt;code&gt;EventBehavior&lt;/code&gt; events for the duration of the run and triggers the publish events the steps returned once, after the last row. A step must not call &lt;code&gt;EventBehaviorConfig::enableEvent()&lt;/code&gt; itself. Report a publish event for every entity you create or change, or Storefront data is not updated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There is no per-run setup or tear-down hook on a step.&lt;/strong&gt; Work that runs once per run, before or after the rows, goes into an &lt;code&gt;ImportPreProcessorPluginInterface&lt;/code&gt; or &lt;code&gt;ImportPostProcessorPluginInterface&lt;/code&gt; plugin (section 4).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An exception fails the run.&lt;/strong&gt; If a step throws, the engine marks the run as &lt;em&gt;failed&lt;/em&gt;, records the exception message as a run error, and re-enables the entity events.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;optional-make-the-type-exportable&quot;&gt;3. Optional: Make the type exportable&lt;/h2&gt;
&lt;p&gt;A job type is exportable when its schema plugin also implements &lt;code&gt;ExportSchemaPluginInterface&lt;/code&gt; and is registered in &lt;code&gt;getExportSchemaPlugins()&lt;/code&gt; (section 4). Both interfaces extend &lt;code&gt;SchemaPluginInterface&lt;/code&gt;, so one class provides &lt;code&gt;getType()&lt;/code&gt;, &lt;code&gt;getSchema()&lt;/code&gt;, and &lt;code&gt;getPlaceholderProvider()&lt;/code&gt; for both sides, and the engine writes a CSV file with the same columns as the import file. The export needs two more classes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INTERFACE&lt;/th&gt;
&lt;th&gt;PURPOSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExportDataProviderInterface&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the rows to export in batches. &lt;code&gt;getBatch(array $columns, int $limit, int $lastId)&lt;/code&gt; returns up to &lt;code&gt;$limit&lt;/code&gt; rows whose primary key is greater than &lt;code&gt;$lastId&lt;/code&gt;, sorted by the primary key in ascending order, each with the key &lt;code&gt;ExportDataProviderInterface::INTERNAL_COLUMN_CURSOR_ID&lt;/code&gt; set to the primary key. The engine takes the cursor of the last row of a batch as the &lt;code&gt;$lastId&lt;/code&gt; of the next call, so an unsorted batch skips rows; it stops at the first empty array, so the provider must return an empty array at the end. The cursor key never reaches the file: the engine writes only the requested &lt;code&gt;$columns&lt;/code&gt; of every row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExportStepInterface&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fills the columns of its concern. &lt;code&gt;exportRows(array $rows, array $columns)&lt;/code&gt; receives the batch and the resolved column headers and returns the rows with the columns filled. The first step initializes the row structure.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ExportStepInterface&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getExportSteps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvExportStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getExportDataProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ExportDataProviderInterface&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerExportDataProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getCustomerQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;If the type is not exportable, register the plugin in &lt;code&gt;getImportSchemaPlugins()&lt;/code&gt; only. The CSV template is still available for its import jobs; an export of such a job is rejected with an error.&lt;/p&gt;
&lt;h2 id=&quot;register-the-schema-plugin&quot;&gt;4. Register the schema plugin&lt;/h2&gt;
&lt;p&gt;Register the plugin in the &lt;code&gt;DataExchange&lt;/code&gt; dependency provider of your project:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/Pyz/Zed/DataExchange/DataExchangeDependencyProvider.php&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Pyz\Zed\DataExchange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pyz\Zed\CustomerDataExchange\Communication\Plugin\DataExchange\CustomerCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spryker\Zed\DataExchange\DataExchangeDependencyProvider&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SprykerDataExchangeDependencyProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SprykerFeature\Zed\ProductExperienceManagement\Communication\Plugin\ProductCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataExchangeDependencyProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SprykerDataExchangeDependencyProvider&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ImportSchemaPluginInterface&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getImportSchemaPlugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;cd&quot;&gt;/**
     * @return array&amp;lt;\Spryker\Zed\DataExchangeExtension\Dependency\Plugin\ExportSchemaPluginInterface&amp;gt;
     */&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getExportSchemaPlugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomerCsvImportSchemaPlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Register the plugin in &lt;code&gt;getExportSchemaPlugins()&lt;/code&gt; only if it implements &lt;code&gt;ExportSchemaPluginInterface&lt;/code&gt; (section 3).&lt;/p&gt;
&lt;p&gt;The same dependency provider offers two optional hooks that run once per import run, before and after the rows are processed: &lt;code&gt;getImportPreProcessorPlugins()&lt;/code&gt; with &lt;code&gt;ImportPreProcessorPluginInterface&lt;/code&gt; and &lt;code&gt;getImportPostProcessorPlugins()&lt;/code&gt; with &lt;code&gt;ImportPostProcessorPluginInterface&lt;/code&gt;. Both receive the &lt;code&gt;ImportJobRunTransfer&lt;/code&gt; of the run.&lt;/p&gt;
&lt;h2 id=&quot;verify-the-new-type&quot;&gt;5. Verify the new type&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;In the Back Office, go to &lt;strong&gt;Import &amp;amp; Export &amp;gt; Import&lt;/strong&gt; and click &lt;strong&gt;Create Import Job&lt;/strong&gt;. Make sure that &lt;strong&gt;customers-csv-import&lt;/strong&gt; is offered in the &lt;strong&gt;Type&lt;/strong&gt; field, and create a job with it.&lt;/li&gt;
&lt;li&gt;In the job row, click &lt;strong&gt;Create Run&lt;/strong&gt;. Under &lt;em&gt;Step 1: Download Template&lt;/em&gt;, click &lt;strong&gt;Download CSV Template&lt;/strong&gt;. The file contains one header row with the columns of &lt;code&gt;getSchema()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fill in a few rows, upload the file under &lt;em&gt;Step 2: Upload Filled CSV&lt;/em&gt; with &lt;strong&gt;Upload &amp;amp; Queue Import&lt;/strong&gt;, and process the run:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;console import:job:run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Open the run. The status is &lt;em&gt;done&lt;/em&gt;, the counters show the processed, successful, and failed rows, and rows rejected by the step are listed with their row number and message.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;related-topics&quot;&gt;Related topics&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html&quot;&gt;Data Exchange Jobs feature overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Install the Data Exchange Jobs feature&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/back-office-product-csv-import-and-export-feature-overview.html&quot;&gt;Back Office product CSV import and export feature overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
            <pubDate>Fri, 30 Oct 2026 18:13:36 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html</guid>
            
            
        </item>
        
        <item>
            <title>Release notes 202608.0</title>
            <description>## B2B Business-Ready Commerce Experiences

### Recurring Orders: general availability {% include badge.html type=&quot;feature&quot; %}

Recurring Orders is now generally available. This release gives buyers real control over an upcoming order before it is placed. They can handle the exceptions that used to break a schedule f.e. swapping in a replacement when a product is discontinued, adjusting quantities, adding products that were missed and decide whether each change applies once or permanently. Budget problems can be corrected by the buyer directly rather than raised as a ticket

**Key capabilities:**
- Adjust an upcoming order before it is placed: replace an item that is no longer available, add or remove lines, and change quantities.
- Choose whether a change applies to the next delivery only or to every delivery from now on.
- Move an order to a different budget or cost center without cancelling the schedule.
- Resolve an order that is in review because of an exceeded budget.
- Proactively editing for Schedule Name, Frequency, Budget &amp; Cost Center, Next Execution date
- Run schedules with up to 200 line items, at the speed buyers expect across list, overview, and review screens.

**Business benefits:**
- Supply continuity. A missed replenishment can stop a production line. Schedules that survive real-world exceptions keep materials arriving without anyone watching the calendar.
- Procurement capacity freed up. Repeat orders are already-made decisions. Automating them lets buyers spend their time on sourcing, not re-keying the same basket.
- Spend stays inside the guardrails. Budget and cost center governance travels with every scheduled order, so automation never becomes a route around approval control.
- Stickier customers. A buyer who runs replenishment through your storefront has embedded you in their production workflow, switching becomes a procurement project, not a price comparison.


**Documentation:**
- [Recurring Orders feature overview](https://docs.spryker.com/docs/pbc/all/order-experience-management/latest/base-shop/feature-overviews/recurring-orders-feature-overview)
- [Install the Recurring Orders feature](https://docs.spryker.com/docs/pbc/all/order-experience-management/latest/base-shop/install-and-upgrade/install-features/install-the-recurring-orders-feature)
- [Recurring Orders GA release](https://api.release.spryker.com/release-group/6689)

### Workflows {% include badge.html type=&quot;feature,early-access&quot; %}

This release introduces a workflows infrastructure that lets teams model and manage workflows for different business entities directly in the Back Office, without requiring code changes or deployment. It provides a reusable, auditable, and versioned workflows foundation built on top of the existing state machine engine.

{% include carousel.html
images=&quot;
https://d2s0ynfc62ej12.cloudfront.net/docs/About/all/releases/release-notes-202608.0.md/workflows_1.jpg||::
https://d2s0ynfc62ej12.cloudfront.net/docs/About/all/releases/release-notes-202608.0.md/workflows_2.jpg||::
https://d2s0ynfc62ej12.cloudfront.net/docs/About/all/releases/release-notes-202608.0.md/workflows_now_next.gif||&quot;
%}

**Key capabilities:**
- Create and adjust workflows directly in the Back Office, including states, transitions, events, conditions, and timeouts that define each process
- Manage workflow versions and control which version is active
- Monitor workflow progress across different entities, with support for project-specific requirements

**Business benefits:**
- Enables business teams to create and adjust workflows faster without developer involvement
- Reduces duplication by replacing one-off workflow implementations with one reusable infrastructure
- Improves governance through versioned definitions and controlled building blocks

**Documentation:**
- [Workflows feature overview](https://docs.spryker.com/docs/pbc/all/back-office/latest/base-shop/workflows-feature-overview)
- [Workflows installation guide](https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-workflow-feature)

### Spryker Design System Storefront {% include badge.html type=&quot;improvement&quot; %}

This release extends the Spryker Design System in the storefront by modernizing the product listing page and merchant profile page. It helps create a more consistent buying journey for B2B customers while giving teams a scalable foundation for future storefront enhancements.

{% include carousel.html
images=&quot;
https://d2s0ynfc62ej12.cloudfront.net/docs/About/all/releases/release-notes-202608.0.md/Design_System_PLP.mp4||&quot;
%}

**Key capabilities:**
- Modernized product listing page based on the B2B design system
- Added a reusable merchant page template for consistent merchant content
- Expanded the storefront design foundation introduced in earlier design-system phases

**Business benefits:**
- Improves product discovery with a more consistent and efficient browsing experience
- Increases storefront visual consistency for B2B buyers
- Reduces future design and development effort through reusable page structures

### Algolia API update {% include badge.html type=&quot;improvement&quot; %}

The Algolia integration has been updated to use the newer PHP client version. This change helps keep the integration aligned with the supported Algolia SDK lifecycle and reduces risk related to the older client reaching the end of SLA.

**Key capabilities:**
- Updates the integration to the newer Algolia PHP client version
- Aligns the integration with Algolia&apos;s current supported SDK direction
- Prepares customers for continued use without relying on an outdated client

**Business benefits:**
- Reduces support and lifecycle risk for customers using the Algolia integration
- Improves confidence in long-term maintainability
- Helps customers stay current with less ambiguity around SDK support status

**Documentation:**
- [Algolia module update](https://github.com/spryker-eco/algolia/releases/tag/2.0.0)
- [Migration guide](https://docs.spryker.com/docs/pbc/all/search/latest/base-shop/third-party-integrations/algolia/upgrade-the-algolia-module)


### Vertex Back Office Configuration {% include badge.html type=&quot;feature&quot; %}

The Vertex configuration page in the Back Office has been updated to reflect the new integration and replace legacy App Composition Platform-oriented fields. This makes the setup clearer and better aligned with the current Vertex integration experience.

**Key capabilities:**
- Updates Vertex configuration fields in the Back Office
- Replaces legacy field structure inherited from the previous ACP-based setup
- Aligns configuration with the new integration model

**Business benefits:**
- Simplifies Vertex setup for business and implementation teams
- Reduces confusion caused by outdated configuration fields
- Helps shorten implementation and configuration time

**Documentation:**
- [Configure Vertex in the Back Office](https://docs.spryker.com/docs/pbc/all/tax-management/latest/base-shop/third-party-integrations/vertex/install-vertex/configure-vertex-in-the-back-office)

## Connected, and AI-Enabled Platform

### AI Dev SDK: Introduce Project Setup Wizard {% include badge.html type=&quot;feature,early-access&quot; %}

The AI Dev SDK now provides a guided setup wizard for new Spryker B2B Marketplace projects based on a fresh demoshop clone. It captures project decisions once and applies them through a resumable setup workflow. This helps teams move faster from initial clone to a verified, project-shaped shop.

&lt;figure class=&quot;video_container&quot;&gt;
    &lt;video width=&quot;100%&quot; height=&quot;auto&quot; controls&gt;
      &lt;source src=&quot;https://d2s0ynfc62ej12.cloudfront.net/docs/About/all/releases/release-notes-202608.0.md/AI_DEV_SDK_project_wizard.mp4&quot; type=&quot;video/mp4&quot;&gt;
  &lt;/video&gt;
&lt;/figure&gt;

**Key capabilities:**
- Runs a guided setup interview covering project identity, services, stores, localization, CI, and run mode
- Applies setup decisions through orchestrated steps with resume support if the process is interrupted
- Produces setup artifacts, verification results, and a summary of remaining manual actions

**Business benefits:**
- Reduces time and effort needed to initialize a new project
- Improves consistency across early project setup decisions
- Gives teams a clearer and more reliable path to a ready-for-scope development environment

**Documentation:**
- [AI Dev SDK Project Starter Wizard](https://docs.spryker.com/docs/dg/dev/ai/ai-dev/ai-dev-project-starter-wizard)

### AI Dev SDK: Introduce Project Upgrade Helper {% include badge.html type=&quot;feature,early-access&quot; %}

The AI Dev SDK now includes an AI-assisted upgrade workflow for customized Spryker projects. It carries a project from its current release to the target one, adopting major module releases into existing project code and verifying the result against a running application. Team checkpoints keep release choice and feature adoption under explicit control.

**Key capabilities:**
- Moves a customized project onto the target Spryker release and resolves the dependency work automatically
- Adopts major module releases into project code by applying the official migration guides to existing customizations
- Realigns customizations, including customized storefront and Back Office screens, with the updated platform
- Verifies the upgraded project against a running application, covering artifact regeneration, tests, and key pages
- Keeps release choice and new feature adoption under team control at defined checkpoints
- Records progress and findings so an upgrade can pause, resume, and run as repeatable CI checks

**Business benefits:**
- Turns weeks of manual upgrade investigation into a guided, largely automated run
- Keeps existing customizations working instead of quietly going dead after an upgrade
- Adopts new platform capabilities only when the team chooses them
- Makes upgrades reviewable and resumable across sprints

**Documentation:**
- [AI Dev SDK Upgrade Workflow](https://docs.spryker.com/docs/dg/dev/ai/ai-dev/ai-dev-upgrade-workflow)

### AI Dev SDK: Autonomous Bugfixer &amp; Improved Customization Workflow {% include badge.html type=&quot;early-access&quot; %} {% include badge.html type=&quot;feature&quot; %}

The AI Dev SDK introduces a new guided workflow for turning bug reports into validated project changes and improves the existing workflow for feature requests. Teams can start with plain-language input and follow an orchestrated path through planning, implementation, verification, and review, while retaining decision-making throughout the workflow and reviewing and approving changes before they are delivered.

**Key capabilities:**
- Improves feature implementation from request intake through acceptance criteria, implementation planning, coding, and verification
- Supports bug fixing from symptom or ticket intake through reproduction, root-cause analysis, minimal fix, testing, QA, and final verification
- Produces traceable reports and supports collaborative or autonomous execution modes

**Business benefits:**
- Speeds up delivery from request or incident to validated change
- Reduces manual handoffs across implementation, QA, and review steps
- Improves consistency and traceability in AI-assisted development workflows

**Documentation:**
- [AI Dev SDK Bugfix Workflow](https://docs.spryker.com/docs/dg/dev/ai/ai-dev/ai-dev-bugfix-workflow)
- [AI Dev SDK Customization Workflow](https://docs.spryker.com/docs/dg/dev/ai/ai-dev/ai-dev-customization-workflow)

### AI Commerce: Configurable Instructions Prompt {% include badge.html type=&quot;improvement,early-access&quot; %}

AI Commerce feature prompts can now be configured consistently through the Back Office across the platform, rather than being hardcoded in the codebase. This applies to capabilities such as Search by Image, Visual Add-to-Cart, the Back Office Assistant, and Smart PIM.

Teams can customise prompts more easily to reflect project-specific language, tone, and business requirements, without requiring deployments or support from developer teams.

**Key capabilities:**
- Moves prompt definitions to configurable project-level settings with default values provided out of the box
- Supports prompt customization without changing the codebase
- Enables easier tuning of AI behavior for specific catalogs, industries, and branding needs

**Business benefits:**
- Makes AI Commerce features easier to adapt for different business contexts
- Reduces custom code and upgrade friction in AI Commerce projects
- Shortens iteration cycles for prompt tuning and testing

**Documentation:**
- [AI Commerce Overview](https://docs.spryker.com/docs/dg/dev/ai/ai-commerce/ai-commerce-overview)

## Efficient and Flexible Cloud Foundation

### Enhancements to Symfony Scheduler {% include badge.html type=&quot;improvement&quot; %}

We improved scheduled job management to give teams more control and better visibility in the Back Office and PaaS environment. You can now manage job execution more easily, run multiple schedules in parallel within the same container, and review richer operational logs and status information.

{% include carousel.html
images=&quot;
https://spryker.s3.eu-central-1.amazonaws.com/docs/About/Releases/release-notes-202608/scheduler2.png||::
https://spryker.s3.eu-central-1.amazonaws.com/docs/About/Releases/release-notes-202608/scheduler1.png||&quot;
%}

**Key capabilities:**
- Start, stop, enable, and disable scheduled jobs from the Back Office
- Run multiple scheduled jobs in parallel in the same container to reduce infrastructure overhead
- View job status details such as running, waiting, error state, worker name, timing, priority, and error logs of the last run

**Business benefits:**
- Reduces operational effort for managing scheduled jobs
- Improves transparency for support and business teams through better job visibility
- Helps optimize infrastructure usage and costs

**Release:**
- [Enhancements to Symfony Scheduler](https://api.release.spryker.com/release-group/6718)

**Documentation:**
- [Integrate Symfony Messenger](https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger)
- [Integrate Symfony Scheduler](https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler)

### Maintenance and Service Updates {% include badge.html type=&quot;improvement&quot; %}

We delivered routine maintenance and service updates across the cloud foundation to keep environments secure, reliable, and aligned with current technology standards. These updates introduce PHP 8.5 as the default runtime version, add initial support for OpenSearch 2.x and 3.x, and establish the foundation for migrating from AWS OpenSearch 1.x.

**Key capabilities:**
- Added PHP 8.5 support to Docker SDK PHP images and enabled it as the default version. PHP 8.2 images are still available, while discontinuing further image generation for that version [after 2026](https://www.php.net/supported-versions.php)
- Added compatibility for indexing, reading, and writing data with newer OpenSearch versions.
- Support for the newer OpenSearch version in Spryker PaaS comes in the next release.

**Business benefits:**
- Improves security and stability through up-to-date platform components
- Helps teams remain aligned with supported PHP and OpenSearch technology versions
- Reduces operational risk and technical debt

**Documentation:**
- [Upgrade to PHP 8.5](https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/upgrade-to-php-85)
- [Supported versions of PHP](https://docs.spryker.com/docs/dg/dev/supported-versions-of-php)
- [OpenSearch migration to 3.5](https://docs.spryker.com/docs/pbc/all/search/latest/base-shop/install-and-upgrade/migrate-from-opensearch-1.3-to-3.5)

### Frontend builder for Yves moved into ShopUi {% include badge.html type=&quot;improvement&quot; %}

We moved the Yves builder into ShopUi and introduced a cleaner extension approach for project-level customization. This makes the builder more reusable, configurable, and easier to maintain across projects.

**Key capabilities:**
- Builder logic is now centralized in ShopUi instead of the project layer
- Project teams can extend entry paths, discovery paths, and namespaces through configuration
- Supports project-level overrides without changing module internals

**Business benefits:**
- Reduces customization effort for frontend builds
- Improves maintainability and reuse across projects
- Simplifies frontend setup while preserving existing storefront behavior

**Documentation:**
- [Frontend builder for Yves v2](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2)
- [Upgrade to frontend builder v2 for Yves](/docs/dg/dev/upgrade-and-migrate/upgrade-to-frontend-builder-v2-for-yves)

### Dynamic multistore locale visibility fixes {% include badge.html type=&quot;improvement&quot; %}

We fixed an issue where adding a new locale via Dynamic Multistore could result in products, category trees, and navigation not being visible as expected.

**Key capabilities:**
- Correctly populates entities with data for newly added locales
- Restores product visibility for newly added locales
- Ensures category trees and navigation are displayed correctly after locale addition


**Business benefits:**
- Reduces setup issues when expanding into new locales
- Helps teams launch localized storefronts with fewer manual corrections
- Ensures consistent and complete entity data across stores and locales

**Documentation:**
- [Adjusted editing navigation for new locale](https://api.release.spryker.com/release-group/6658)

### Customer login consistency improvements {% include badge.html type=&quot;improvement&quot; %}

We fixed an issue that could break login behavior when a customer signed in from different places.

**Key capabilities:**
- Improves login handling across multiple customer sessions
- Prevents disruptions caused by parallel or distributed login activity
- Stabilizes customer authentication behavior

**Business benefits:**
- Reduces customer login friction
- Improves reliability of the customer account experience
- Lowers support effort related to authentication issues

**Documentation:**
- [Fixed session handler to avoid behavior inconsistency](https://api.release.spryker.com/release-group/6656)

### Reservation aggregation across multiple plugins {% include badge.html type=&quot;improvement&quot; %}

We introduced a new, more flexible plugin stack for reservation calculation. The new mechanism executes all configured reservation aggregation plugins, ensuring that reservation data is processed completely and accurately.

**Key capabilities:**
- Executes multiple reservation aggregation plugins in sequence
- Prevents incomplete aggregation when reservations span multiple stores or warehouses
- Avoids incorrect merging behavior during reservation processing
- Provides a more flexible foundation for extending reservation calculation logic

**Business benefits:**
- Improves inventory and reservation accuracy
- Supports more complex multi-store and warehouse setups
- Reduces risk of stock inconsistencies

**Documentation:**
- [Install the Packaging Units feature](https://docs.spryker.com/docs/pbc/all/product-information-management/latest/base-shop/install-and-upgrade/install-features/install-the-packaging-units-feature#set-up-behavior)
- [Install the Marketplace Inventory Management + Order Management feature](https://docs.spryker.com/docs/pbc/all/warehouse-management-system/latest/marketplace/install-features/install-the-marketplace-inventory-management-order-management-feature#prerequisites)
- [Install the Marketplace Inventory Management + Packaging Units feature](https://docs.spryker.com/docs/pbc/all/warehouse-management-system/latest/marketplace/install-features/install-the-marketplace-inventory-management-packaging-units-feature#set-up-behavior)
- [Multistore reservation aggregation](https://api.release.spryker.com/release-group/6611)
- [Fix reservation aggregation request expander](https://api.release.spryker.com/release-group/6676)

### API Platform nested object schema support {% include badge.html type=&quot;improvement&quot; %}

We extended API Platform resource YAML so a list of objects can declare its element shape. A `type: array` property with a sibling items block now publishes the element as a referenced component schema, instead of a bare untyped array. Single nested objects were already typed; this closes the gap for collections.

Key capabilities:
- Declare `items: {type: object, properties: …}` alongside `type: array` in a resource schema
- The generated OpenAPI document references a generated element schema and registers it in the same document
- Lists of scalars and canonical (shared) object shapes are supported
- Existing providers keep working — assigning raw arrays is still valid; no provider rewrite required

Adoption is per field, and opt-in for a reason: a typed element is a closed shape. Adding items to a list that is already part of a released response silently drops every payload key you did not declare. Before adopting an existing field, diff a real response payload against your intended items.properties. New fields are unaffected. Storefront APIs are unchanged.

Note: openapiContext.items is documentation passthrough only — it generates nothing. Only a sibling of type: array produces a typed element schema.

Documentation:
- [Typed collections in the published contract](/docs/integrations/spryker-api/api-platform/typed-collections.html)
- [Object collections](/docs/integrations/spryker-api/api-platform/resource-schemas.html#object-collections)

### Graceful handling of email delivery failures {% include badge.html type=&quot;improvement&quot; %}

We improved email sending mechanisms across Storefront, Back Office, Merchant Portal, and API to handle delivery failures gracefully without causing system errors or disrupting application workflows.

**Key capabilities:**
- Prevents email delivery failures from interrupting or causing the failure of primary business processes, including cases where email addresses or domains are not whitelisted in AWS SES sandbox mode.
- Displays clear, non-disruptive error messages with error codes in the Storefront, Merchant Portal, and Back Office, while allowing the primary user action to complete successfully.
- Records comprehensive details for every email delivery failure, including the email type, sender, recipients, and relevant exception details.
- Logs email delivery failures that occur during Order Management System (OMS) processing without halting execution or leaving orders stuck.
- Standardizes REST/Glue API responses to return a 422 Unprocessable Entity status code instead of a 500 Internal Server Error

**Business benefits:**
- Delivers a smooth user experience by preventing app crashes during email failure scenarios
- Reduces support overhead and troubleshooting effort for technical and operations teams

**Documentation:**
- [Email service](/docs/ca/dev/email-service/email-service)
- [Email deliverability](/docs/ca/dev/email-service/email-deliverability)

### Community contributions {% include badge.html type=&quot;improvement&quot; %}

We included several community-driven improvements in this release. These contributions help improve compatibility and code quality in commonly used modules.

**Key capabilities:**
- Made QuoteApproval compatible with PropelBehavior
- Replaced docblocks with typed class constants in Transfer
- Added support for sensitive properties in Transfer

**Business benefits:**
- Brings practical enhancements contributed by the community
- Improves maintainability, security, and compatibility in key modules
- Strengthens collaboration across the Spryker ecosystem

**Documentation:**
- [New community PR: Make QuoteApproval compatible to PropelBehavior](https://api.release.spryker.com/release-group/6664)
- [New community PR: Add support of sensitive properties](https://api.release.spryker.com/release-group/6657)
</description>
            <pubDate>Fri, 30 Oct 2026 14:03:18 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/release-notes-202608.0.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/release-notes-202608.0.html</guid>
            
            
        </item>
        
        <item>
            <title>Data Exchange</title>
            <description>&lt;p&gt;Data Exchange refers to the process of transferring data between Spryker and third-party systems.&lt;/p&gt;
&lt;p&gt;Spryker offers the following options to import and export data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data Exchange Jobs: an installable SCCOS feature, see &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Install the Data Exchange Jobs feature&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Data Exchange API: available in SCCOS by default&lt;/li&gt;
&lt;li&gt;Data Importers and Data Exporters: available in Spryker Cloud Commerce OS (SCCOS) by default&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;data-exchange-jobs&quot;&gt;Data Exchange Jobs&lt;/h2&gt;
&lt;p&gt;Data Exchange Jobs let business users move data into and out of SCCOS in bulk through files, without development effort per file. A job defines the data type and the columns of the file, every uploaded file is processed asynchronously as a run, and every rejected row is reported with its row number and reason. Jobs, runs, exports, and CSV templates are managed in the Back Office under &lt;strong&gt;Import &amp;amp; Export&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The engine is independent of the data it moves. Products are available out of the box through the &lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/product-experience-management.html&quot;&gt;Product Experience Management&lt;/a&gt; capability, and developers add further data types with one schema plugin per type.&lt;/p&gt;
&lt;p&gt;For details, see &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html&quot;&gt;Data Exchange Jobs feature overview&lt;/a&gt; and &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Install the Data Exchange Jobs feature&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;data-exchange-api&quot;&gt;Data Exchange API&lt;/h2&gt;
&lt;p&gt;Data Exchange API is a dynamic database API that facilitates data transfer in real-time, ensuring data is exchanged across all integrated platforms. It’s part of the SCCOS platform core.&lt;/p&gt;
&lt;p&gt;Data Exchange API lets you build, customize, and manage database APIs tailored to your specific business requirements through a user interface.&lt;/p&gt;
&lt;p&gt;The main benefits of the Data Exchange API include the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No coding is required: The API endpoints are created from the user interface.&lt;/li&gt;
&lt;li&gt;Rapid API generation: The APIs are generated within minutes.&lt;/li&gt;
&lt;li&gt;Flexibility and customization: You can tailor APIs to your needs. You can define parameters to ensure compatibility with your systems.&lt;/li&gt;
&lt;li&gt;Real-time updates: The infrastructure supports dynamic changes, so you can modify APIs on the fly.&lt;/li&gt;
&lt;li&gt;Security and Access Control: The infrastructure incorporates strong security measures and access controls, which safeguards sensitive information.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We recommend considering the Data Exchange API if you want to create your own data integration engine via API, without using any middleware software.&lt;/p&gt;
&lt;h2 id=&quot;data-importers-and-data-exporters&quot;&gt;Data Importers and Data Exporters&lt;/h2&gt;
&lt;p&gt;Data Importers and Data Exporters are tools that let you bring external data into and send data from SCCOS, using .CSV files.  Data Importers and Data Exporters are part of the SCCOS platform core.
Data Importers and Data Exporters require extensive customization development for each project and ongoing development effort, which makes them less suitable for demanding data exchange.&lt;/p&gt;
&lt;p&gt;For information on how Data importers and Exporters work, see &lt;a href=&quot;/docs/dg/dev/data-import/latest/data-import.html&quot;&gt;Data import&lt;/a&gt; and &lt;a href=&quot;/docs/pbc/all/order-management-system/latest/base-shop/import-and-export-data/orders-data-export/orders-data-export.html&quot;&gt;Data export&lt;/a&gt;.&lt;/p&gt;
</description>
            <pubDate>Thu, 29 Oct 2026 15:27:47 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/data-exchange/latest/data-exchange.html</guid>
            
            
        </item>
        
        <item>
            <title>Back Office product CSV import and export feature overview</title>
            <description>&lt;p&gt;The &lt;em&gt;Back Office Product CSV Import and Export&lt;/em&gt; feature lets Back Office users import and export product data in bulk through CSV files. It is the product data type of the &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html&quot;&gt;Data Exchange Jobs&lt;/a&gt; feature: the engine stores import jobs and runs, processes uploaded files in batches, records per-row errors, and provides the Back Office &lt;strong&gt;Import &amp;amp; Export&lt;/strong&gt; pages, while this feature contributes the &lt;em&gt;products-csv-import&lt;/em&gt; job type with its column schema, import steps, and export steps through &lt;code&gt;ProductCsvImportSchemaPlugin&lt;/code&gt; of the &lt;code&gt;ProductExperienceManagement&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;For the import and export workflow, the template download, and the error handling, see &lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/data-exchange-jobs-feature-overview.html&quot;&gt;Data Exchange Jobs feature overview&lt;/a&gt;. To manage products programmatically instead of through files, see &lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/products-backend-api.html&quot;&gt;Products Backend API&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;supported-product-data&quot;&gt;Supported product data&lt;/h2&gt;
&lt;p&gt;For the &lt;em&gt;products-csv-import&lt;/em&gt; schema, the following data is imported and exported:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DATA&lt;/th&gt;
&lt;th&gt;IMPORT&lt;/th&gt;
&lt;th&gt;EXPORT&lt;/th&gt;
&lt;th&gt;SCOPE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abstract and concrete products&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Core product data, approval status, active state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localized names and descriptions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per locale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Localized attributes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per locale, key=value format separated by semicolons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store assignments&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Semicolon-separated store names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Categories&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Category keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tax sets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Tax set name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per locale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prices&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per price mode, store, currency, and dimension (gross/net)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per warehouse, numeric quantity or NOOS (Never Out Of Stock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shipment types&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Shipment type keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product images&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Per locale and sort order, for both abstract and concrete products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merchant assignments&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Merchant reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;import-file-structure&quot;&gt;Import file structure&lt;/h2&gt;
&lt;p&gt;To check the expected file structure, export the existing products of the import job. The exported file uses the same structure as the import file, so it shows how abstract and concrete products of your shop are represented across rows and which values the columns contain.&lt;/p&gt;
&lt;p&gt;Each CSV row describes either an abstract product or a concrete product, never both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A row with &lt;em&gt;Abstract SKU&lt;/em&gt; filled in and &lt;em&gt;Concrete SKU&lt;/em&gt; empty creates or updates the abstract product.&lt;/li&gt;
&lt;li&gt;A row with both &lt;em&gt;Abstract SKU&lt;/em&gt; and &lt;em&gt;Concrete SKU&lt;/em&gt; filled in creates or updates the concrete product and assigns it to the abstract product identified by the abstract SKU.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To import an abstract product together with its concrete products in one file, place them in separate rows: one row for the abstract product, followed by one row per concrete product. The abstract product is imported before the concrete products, so both can be in the same file:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ABSTRACT SKU&lt;/th&gt;
&lt;th&gt;CONCRETE SKU&lt;/th&gt;
&lt;th&gt;PRODUCT STATUS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;001_25904006&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;001_25904007&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;001_25904008&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;An abstract product row is only needed for abstract products you want to create or update. If the abstract product already exists in the system, you can import concrete products for it without adding an abstract product row: enter the SKU of the existing abstract product in the &lt;em&gt;Abstract SKU&lt;/em&gt; column of each concrete product row. The following file adds two concrete products to the existing abstract product &lt;code&gt;001&lt;/code&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ABSTRACT SKU&lt;/th&gt;
&lt;th&gt;CONCRETE SKU&lt;/th&gt;
&lt;th&gt;PRODUCT STATUS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;001_25904006&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;001_25904007&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;section class=&apos;info-block &apos;&gt;&lt;i class=&apos;info-block__icon icon-info&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Info&lt;/div&gt;
&lt;p&gt;If a row contains a concrete SKU but no abstract SKU, the concrete SKU is also used as the abstract SKU. In this case, the abstract product with that SKU must already exist.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;p&gt;Because the row type determines which data is imported, some columns are only processed in one of the two row types:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;COLUMNS&lt;/th&gt;
&lt;th&gt;PROCESSED IN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Merchant&lt;/em&gt;, &lt;em&gt;Stores&lt;/em&gt;, &lt;em&gt;Categories&lt;/em&gt;, &lt;em&gt;Tax Set Name&lt;/em&gt;, &lt;em&gt;URL ({locale})&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Abstract product rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Stock ({warehouse})&lt;/em&gt;, &lt;em&gt;Shipment Types&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Concrete product rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Product Status&lt;/em&gt;, &lt;em&gt;Name ({locale})&lt;/em&gt;, &lt;em&gt;Description ({locale})&lt;/em&gt;, &lt;em&gt;Attributes ({locale})&lt;/em&gt;, &lt;em&gt;Price (…)&lt;/em&gt;, &lt;em&gt;Image {size} (…)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Both row types&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;product-statuses&quot;&gt;Product statuses&lt;/h2&gt;
&lt;p&gt;The values accepted in the &lt;em&gt;Product Status&lt;/em&gt; column depend on the row type. Values are case insensitive.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ROW TYPE&lt;/th&gt;
&lt;th&gt;ACCEPTED VALUES&lt;/th&gt;
&lt;th&gt;RESULT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abstract product row: &lt;em&gt;Concrete SKU&lt;/em&gt; is empty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;waiting_for_approval&lt;/code&gt;, &lt;code&gt;approved&lt;/code&gt;, &lt;code&gt;denied&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the approval status of the abstract product.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete product row: &lt;em&gt;Concrete SKU&lt;/em&gt; is filled in&lt;/td&gt;
&lt;td&gt;&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;inactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;active&lt;/code&gt; activates the concrete product, &lt;code&gt;inactive&lt;/code&gt; deactivates it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Product Status&lt;/em&gt; is required in every row. If the value is empty or does not belong to the accepted values for the row type, the row is skipped and an error is reported. For example, &lt;code&gt;active&lt;/code&gt; in an abstract product row and &lt;code&gt;approved&lt;/code&gt; in a concrete product row both fail validation.&lt;/p&gt;
&lt;p&gt;The approval statuses of abstract products come from the &lt;a href=&quot;/docs/pbc/all/product-information-management/latest/base-shop/install-and-upgrade/install-features/install-the-product-approval-process-feature.html&quot;&gt;Product Approval Process&lt;/a&gt; feature, which the Product Experience Management feature requires. The statuses accepted for concrete products, and the one that activates a concrete product, are defined by &lt;code&gt;getProductConcreteStatuses()&lt;/code&gt; and &lt;code&gt;getProductConcreteStatusActive()&lt;/code&gt; in the module’s &lt;code&gt;ProductExperienceManagementConfig&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;placeholders-in-the-product-schema&quot;&gt;Placeholders in the product schema&lt;/h2&gt;
&lt;p&gt;The product schema uses placeholders in its column headers. The values come from &lt;code&gt;ProductCsvExportPlaceholderProvider&lt;/code&gt; of the &lt;code&gt;ProductExperienceManagement&lt;/code&gt; module and reflect the data of the shop at the time the template or the export is generated.&lt;/p&gt;
&lt;div class=&quot;width-100&quot;&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;COLUMN PATTERN&lt;/th&gt;
&lt;th&gt;PLACEHOLDER VALUES&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name ({locale})&lt;/code&gt;, &lt;code&gt;Description ({locale})&lt;/code&gt;, &lt;code&gt;URL ({locale})&lt;/code&gt;, &lt;code&gt;Attributes ({locale})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{locale}&lt;/code&gt;: every locale of the stores.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Price ({price_mode}-{store}-{currency}-{price_dimension})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{price_mode}&lt;/code&gt;: the price modes; &lt;code&gt;{store}&lt;/code&gt;: every store; &lt;code&gt;{currency}&lt;/code&gt;: every currency of the stores; &lt;code&gt;{price_dimension}&lt;/code&gt;: &lt;code&gt;Gross&lt;/code&gt; and &lt;code&gt;Net&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stock ({warehouse})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{warehouse}&lt;/code&gt;: every warehouse.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Image {size} ({locale}-{sort_order})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{size}&lt;/code&gt;: &lt;code&gt;Small&lt;/code&gt; and &lt;code&gt;Large&lt;/code&gt;; &lt;code&gt;{locale}&lt;/code&gt;: every locale; &lt;code&gt;{sort_order}&lt;/code&gt;: the sort orders of the product images in the shop.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;related-developer-documents&quot;&gt;Related Developer documents&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INSTALLATION GUIDES&lt;/th&gt;
&lt;th&gt;UPGRADE GUIDES&lt;/th&gt;
&lt;th&gt;TUTORIALS AND HOWTOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/install/install-the-back-office-product-csv-import-and-export-feature.html&quot;&gt;Install the Back Office Product CSV Import and Export feature&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/product-experience-management/latest/install/upgrade-the-productexperiencemanagement-module.html&quot;&gt;Upgrade the ProductExperienceManagement module&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/tutorials-and-howtos/add-a-custom-data-type-to-data-exchange-jobs.html&quot;&gt;Add a custom data type to Data Exchange Jobs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;/docs/pbc/all/data-exchange/latest/data-exchange-jobs/install/install-the-data-exchange-jobs-feature.html&quot;&gt;Install the Data Exchange Jobs feature&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
            <pubDate>Thu, 29 Oct 2026 15:27:47 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/back-office-product-csv-import-and-export-feature-overview.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/product-experience-management/latest/back-office-product-csv-import-and-export-feature-overview.html</guid>
            
            
        </item>
        
        <item>
            <title>Storefront Assistant</title>
            <description>&lt;style&gt;
  .post-content pre {
    white-space: pre;
    word-break: normal;
    overflow-x: auto;
  }
&lt;/style&gt;

Storefront Assistant is an AI chat for logged-in Storefront customers. It answers shopping questions only from the shop catalog and streams each answer to the browser as Server-Sent Events (SSE).

For the business overview, see [Storefront Assistant](/docs/pbc/all/ai-commerce/latest/storefront-assistant.html).

## Architecture

![Storefront Assistant architecture: the browser, Yves with the Client layer, Zed, and the AI providers](https://spryker.s3.eu-central-1.amazonaws.com/docs/dg/dev/ai-commerce/storefront-assistant-architecture.png)

Storefront Assistant runs in the Yves application. The chat logic, the agents, the tools, and the call to the AI provider run in the Client layer, inside the Yves process. Zed does not call the AI provider. Zed only loads the AI configuration and the conversation history, saves the conversation, and writes the audit log.

&lt;div class=&quot;width-100&quot;&gt;

| PART | IMPLEMENTATION | LAYER |
|------|----------------|-------|
| Chat widget | `StorefrontAssistantWidget`, which renders the `storefront-assistant` organism. The organism embeds the shared chat of the module. | Yves |
| Header button | The `storefront-assistant-trigger` molecule, included by the project `header` organism | Yves |
| Endpoints | `StorefrontAssistantController`, registered by `StorefrontAssistantRouteProviderPlugin` | Yves |
| Agents, tools, and SSE plugins | `SprykerFeature\Client\AiCommerce\Plugin\...` | Client |
| Streaming, tool loop, and call to the AI provider | `AiFoundationClientInterface::streamPrompt()` | Client |
| AI configuration, chat history storage, and audit log | The Zed gateway of the `AiFoundation` module and its `spy_ai_conversation_history` table | Zed |
| Conversation list and index | Key-value storage (Redis) with a time to live (TTL) | Client |

&lt;/div&gt;

For each chat turn, the following happens:

1. The browser sends `POST /shopping-assistant/prompt` with the message, the conversation reference, and the page context.
2. The controller validates the request and opens an SSE stream.
3. The agent selector picks one agent for the turn. For details, see [Agent selection](/docs/dg/dev/ai/ai-commerce/storefront-assistant/storefront-assistant-agents.html#agent-selection).
4. The agent calls `AiFoundationClientInterface::streamPrompt()` with its system prompt and tool names.
5. `streamPrompt()` asks Zed for the AI configuration and the conversation history.
6. The Client calls the AI provider directly and runs the tools of the model in the Yves process.
7. While the model answers, the SSE plugins write the text, the reasoning, and the tool events to the stream.
8. The Client saves the turn in the chat history through Zed.

For details on how `streamPrompt()` sends the model output, the tool calls, and the conversation history, see [Stream AI responses with the AiFoundation module](/docs/dg/dev/ai/ai-foundation/ai-foundation-stream-prompt.html).

The browser keeps only a small state in `localStorage` under the `storefront_assistant_state` key: `isOpen`, `isExpanded`, `conversationReference`, `userSelectedAgent`, and `activeAgent`. The browser does not keep the messages. It loads them from the server on each page load.

## Agents

An agent is a plugin that has its own system prompt, tool list, and Back Office toggle. For the shipped agents, their tools, the registration, and the agent selection rules, see [Storefront Assistant agents](/docs/dg/dev/ai/ai-commerce/storefront-assistant/storefront-assistant-agents.html).

## SSE plugins

The following Client plugins write the stream events. They are registered in the Client `AiFoundationDependencyProvider`:

&lt;div class=&quot;width-100&quot;&gt;

| PLUGIN | STACK | DESCRIPTION |
|--------|-------|-------------|
| `StorefrontAssistantSsePreToolCallPlugin` | `getPreToolCallPlugins()` | Sends `tool-input-start` and `tool-input-available` before each tool call. |
| `StorefrontAssistantSsePostToolCallPlugin` | `getPostToolCallPlugins()` | Sends `tool-output-available` or `tool-output-denied` after each customer-facing tool call. |
| `StorefrontAssistantSseStreamEventPlugin` | `getStreamEventPlugins()` | Sends the text and reasoning deltas of the model. |

&lt;/div&gt;

These plugins implement the `AiFoundation` streaming plugin interfaces. For the plugin contract, see [Stream AI responses with the AiFoundation module](/docs/dg/dev/ai/ai-foundation/ai-foundation-stream-prompt.html#plugin-reference).

The SSE plugins send events only for the AI configurations that `AiCommerceConfig::getStorefrontAssistantSseAiConfigurationNames()` returns. A turn on any other AI configuration streams only `start`, `data-agent`, `finish`, and `[DONE]`.

## Endpoints

`StorefrontAssistantRouteProviderPlugin` adds the following routes:

&lt;div class=&quot;width-100&quot;&gt;

| METHOD | PATH | ROUTE NAME | CSRF HEADER | RESPONSE |
|--------|------|------------|-------------|----------|
| `POST` | `/shopping-assistant/prompt` | `shopping-assistant/prompt` | Yes | SSE stream |
| `GET` | `/shopping-assistant/conversations` | `shopping-assistant/conversations` | No | JSON |
| `GET` | `/shopping-assistant/conversations/messages` | `shopping-assistant/conversations/messages` | No | JSON |
| `DELETE` | `/shopping-assistant/conversations` | `shopping-assistant/conversations/delete` | Yes | JSON |

&lt;/div&gt;

Requests with a CSRF header must send the `X-CSRF-Token` header with a token for the `storefront_assistant` token ID. The widget reads the token from the page.

All endpoints require a logged-in customer. When the **Enable Storefront Assistant** setting is off, all endpoints return `404`.

### Prompt request

The body of `POST /shopping-assistant/prompt` is a JSON object:

```json
{
  &quot;message&quot;: &quot;Show me cameras under 500 euros&quot;,
  &quot;conversationReference&quot;: &quot;c_7f3a9b&quot;,
  &quot;selectedAgent&quot;: &quot;product_discovery&quot;,
  &quot;pageContext&quot;: { &quot;pageType&quot;: &quot;category&quot;, &quot;categoryName&quot;: &quot;Cameras&quot;, &quot;filters&quot;: [&quot;Canon&quot;] },
  &quot;attachments&quot;: [{ &quot;mediaType&quot;: &quot;image/png&quot;, &quot;content&quot;: &quot;&lt;base64&gt;&quot;, &quot;name&quot;: &quot;photo.png&quot; }]
}
```

&lt;div class=&quot;width-100&quot;&gt;

| FIELD | REQUIRED | RULE |
|-------|----------|------|
| `message` | Yes | Must not be empty. The maximum length comes from `getStorefrontAssistantMaxMessageLengthCharacters()`, which is calculated from the conversation history context window of `AiFoundation`. |
| `conversationReference` | Yes | Generated by the browser. Must match `/\A[A-Za-z0-9_-]+\z/` and have a maximum of 64 characters. |
| `selectedAgent` | No | The agent name. An empty or missing value means **Auto**. |
| `pageContext` | No | `pageType` is `product`, `category`, or `search`. The server removes the context for other values. |
| `attachments` | No | A maximum of five files of 5&amp;nbsp;MiB each. Allowed types: JPEG, PNG, WebP, GIF, and PDF. The server checks the binary signature of each file. |

&lt;/div&gt;

## Stream protocol

The prompt endpoint follows the [AI SDK UI Message Stream Protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol). This lets you replace the shipped widget with any client that understands this protocol.

&lt;div class=&quot;width-100&quot;&gt;

| ELEMENT | VALUE |
|---------|-------|
| Transport | SSE over the HTTP response body of the `POST` request |
| Frame | `data: &lt;JSON object&gt;\n\n`, one JSON part for each frame, without `event:` or `id:` lines |
| Part type | The `type` field of the JSON object |
| End of stream | `data: [DONE]\n\n` |
| Writer | `SprykerFeature\Shared\AiCommerce\Stream\StreamEventEmitter`, which flushes the output after each frame |

&lt;/div&gt;

A successful stream has the following response headers:

&lt;div class=&quot;width-100&quot;&gt;

| HEADER | VALUE | PURPOSE |
|--------|-------|---------|
| `Content-Type` | `text/event-stream` | Marks the response as SSE. |
| `Cache-Control` | `no-cache` | Prevents caching of the stream. |
| `X-Accel-Buffering` | `no` | Disables nginx buffering, so each frame reaches the browser immediately. |

&lt;/div&gt;

{% info_block infoBox &quot;Protocol header&quot; %}

The protocol recommends the `x-vercel-ai-ui-message-stream: v1` response header for custom backends. Storefront Assistant does not send this header, and the shipped widget does not need it. If you connect a client that checks this header, such as the AI SDK `useChat` hook, add the header in a project-level controller.

{% endinfo_block %}

### Part types

&lt;div class=&quot;width-100&quot;&gt;

| PART TYPE | FIELDS | WHEN |
|-----------|--------|------|
| `start` | `messageId` | Always, as the first part. |
| `data-agent` | `data.name` | After the agent selection, before the model call. |
| `reasoning-start` | `id` | At the first reasoning chunk of a block. Sent only when `isStorefrontAssistantReasoningStreamed()` returns `true`. |
| `reasoning-delta` | `id`, `delta` | For each reasoning chunk of the model. |
| `reasoning-end` | `id` | When a text block, a tool call, an error, or the end of the turn follows. |
| `text-start` | `id` | At the first text chunk of a block. |
| `text-delta` | `id`, `delta` | For each answer text chunk. |
| `text-end` | `id` | When a reasoning block, a tool call, an error, or the end of the turn follows. |
| `tool-input-start` | `toolCallId`, `toolName` | Before each tool call, both research and customer-facing. |
| `tool-input-available` | `toolCallId`, `toolName`, `input` | Directly after `tool-input-start`. `input` contains the arguments that the model sent. |
| `tool-output-available` | `toolCallId`, `output.result` | After a customer-facing tool call. |
| `tool-output-denied` | `toolCallId` | When a customer-facing tool was not allowed to run. |
| `error` | `errorText` | On a validation error, when no agent is enabled, on a vendor failure, or on an exception. The server closes the open text or reasoning block first. |
| `finish` | — | Always, as the last part, also after an `error`. |

&lt;/div&gt;

The server follows these rules:

- Each turn has exactly one `start` part and one `finish` part.
- Only one text or reasoning block is open at a time. A new block, a tool call, or an error closes the open block first.
- Each block `id` is unique in the turn, for example `text_1`, `reasoning_2`, `text_3`.
- The `tool-input-*` and `tool-output-*` parts of one call have the same `toolCallId`.
- A research tool, such as `catalog_search`, sends `tool-input-*` parts but no `tool-output-*` part. The widget uses the input parts only for the progress label.

The server does not send these protocol part types: `start-step`, `finish-step`, `tool-input-delta`, `source-url`, `file`, and `abort`.

### Example stream

The customer sends &quot;Show me cameras under 500 euros&quot;:

```text
data: {&quot;type&quot;:&quot;start&quot;,&quot;messageId&quot;:&quot;msg_6703a1f2b4c5d&quot;}

data: {&quot;type&quot;:&quot;data-agent&quot;,&quot;data&quot;:{&quot;name&quot;:&quot;product_discovery&quot;}}

data: {&quot;type&quot;:&quot;reasoning-start&quot;,&quot;id&quot;:&quot;reasoning_1&quot;}

data: {&quot;type&quot;:&quot;reasoning-delta&quot;,&quot;id&quot;:&quot;reasoning_1&quot;,&quot;delta&quot;:&quot;The customer wants cameras with a price limit of 500 EUR.&quot;}

data: {&quot;type&quot;:&quot;reasoning-end&quot;,&quot;id&quot;:&quot;reasoning_1&quot;}

data: {&quot;type&quot;:&quot;tool-input-start&quot;,&quot;toolCallId&quot;:&quot;call_a1b2c3d4&quot;,&quot;toolName&quot;:&quot;catalog_search&quot;}

data: {&quot;type&quot;:&quot;tool-input-available&quot;,&quot;toolCallId&quot;:&quot;call_a1b2c3d4&quot;,&quot;toolName&quot;:&quot;catalog_search&quot;,&quot;input&quot;:{&quot;query&quot;:&quot;camera&quot;,&quot;priceMax&quot;:50000}}

data: {&quot;type&quot;:&quot;tool-input-start&quot;,&quot;toolCallId&quot;:&quot;call_e5f6a7b8&quot;,&quot;toolName&quot;:&quot;display_products&quot;}

data: {&quot;type&quot;:&quot;tool-input-available&quot;,&quot;toolCallId&quot;:&quot;call_e5f6a7b8&quot;,&quot;toolName&quot;:&quot;display_products&quot;,&quot;input&quot;:{&quot;idProductAbstracts&quot;:[201,204,210]}}

data: {&quot;type&quot;:&quot;tool-output-available&quot;,&quot;toolCallId&quot;:&quot;call_e5f6a7b8&quot;,&quot;output&quot;:{&quot;result&quot;:&quot;{\&quot;products\&quot;:[{\&quot;idProductAbstract\&quot;:201,\&quot;name\&quot;:\&quot;Canon IXUS 160\&quot;,\&quot;sku\&quot;:\&quot;201\&quot;,\&quot;url\&quot;:\&quot;/en/canon-ixus-160-201\&quot;,\&quot;priceFormatted\&quot;:\&quot;€349.00\&quot;,\&quot;rating\&quot;:4.5,\&quot;reviewCount\&quot;:12}],\&quot;displayedCount\&quot;:3,\&quot;refinementChips\&quot;:[{\&quot;label\&quot;:\&quot;Canon\&quot;,\&quot;filterArgument\&quot;:\&quot;filters.brand\&quot;,\&quot;value\&quot;:\&quot;Canon\&quot;,\&quot;matchCount\&quot;:5}],\&quot;seeAll\&quot;:{\&quot;totalResults\&quot;:18,\&quot;queryString\&quot;:\&quot;q=camera&amp;price%5Bmax%5D=500\&quot;}}&quot;}}

data: {&quot;type&quot;:&quot;text-start&quot;,&quot;id&quot;:&quot;text_2&quot;}

data: {&quot;type&quot;:&quot;text-delta&quot;,&quot;id&quot;:&quot;text_2&quot;,&quot;delta&quot;:&quot;These three cameras fit your budget. &quot;}

data: {&quot;type&quot;:&quot;text-delta&quot;,&quot;id&quot;:&quot;text_2&quot;,&quot;delta&quot;:&quot;Narrow by brand to see fewer models.&quot;}

data: {&quot;type&quot;:&quot;text-end&quot;,&quot;id&quot;:&quot;text_2&quot;}

data: {&quot;type&quot;:&quot;finish&quot;}

data: [DONE]
```

The values in this example are illustrations. The field names are the real names.

A failed turn, for example without an API token, looks like this:

```text
data: {&quot;type&quot;:&quot;start&quot;,&quot;messageId&quot;:&quot;msg_6703a1f2b4c5e&quot;}

data: {&quot;type&quot;:&quot;data-agent&quot;,&quot;data&quot;:{&quot;name&quot;:&quot;product_discovery&quot;}}

data: {&quot;type&quot;:&quot;error&quot;,&quot;errorText&quot;:&quot;The shopping assistant is temporarily unavailable. Please try again.&quot;}

data: {&quot;type&quot;:&quot;finish&quot;}

data: [DONE]
```

The `errorText` value is a glossary translation in the locale of the request. The server writes the vendor error to the log, not to the stream.

### Tool results

The `output.result` value is a JSON-encoded string, not a JSON object. Decode it a second time. The content depends on the tool:

&lt;div class=&quot;width-100&quot;&gt;

| TOOL | KEYS OF THE DECODED RESULT |
|------|---------------------------|
| `display_products` | `products[]`, `displayedCount`, and optionally `unknownIdProductAbstracts[]` and `alreadyShown`. When the products come from a `catalog_search` call of the same turn, also `refinementChips[]`, `activeFilters[]`, and `seeAll`. |
| `compare_products` | `comparedProducts[]`, `attributeRows[]` with `attribute`, `values[]`, and `isDifferent`, and optionally `comparisonUrl`, `ignoredIdProductAbstracts[]`, and `unknownIdProductAbstracts[]`. On failure, `error` or `alreadyShown`. |
| `offer_choices` | `question`, `choices[]`, and `nextStep`. On failure, `error`. |
| `display_setup` | `products[]`, `totalFormatted`, and `budgetFormatted`. On failure, `error`, and `items[]` when the setup exceeds the budget. |

&lt;/div&gt;

A product card has the following fields: `idProductAbstract`, `name`, `sku`, `url`, `imageUrl`, `description`, `price`, `priceFormatted`, `originalPrice`, `originalPriceFormatted`, `currencyIsoCode`, `priceMode`, `rating`, `reviewCount`, `labelIds`, `labels`, `attributes`, `variants`, `variantCount`, `reason`, and `isPriceOutlier`. Plugins in `getStorefrontAssistantProductCollectionExpanderPlugins()` can add more fields.

&lt;div class=&quot;width-100&quot;&gt;

| OBJECT | FIELDS |
|--------|--------|
| Refinement chip in `refinementChips[]` and `activeFilters[]` | `label`, `filterArgument`, `value`, `matchCount` |
| `seeAll` | `totalResults`, `queryString`, and `categoryUrl` when the search had a category. Sent only when more products match than the chat shows. |

&lt;/div&gt;

### Conversation history endpoints

The history endpoints return plain JSON:

&lt;div class=&quot;width-100&quot;&gt;

| METHOD AND PATH | REQUEST | RESPONSE |
|-----------------|---------|----------|
| `GET /shopping-assistant/conversations` | Optional query parameter `conversationReference` or `conversationReferences[]` | `{&quot;conversations&quot;:[{&quot;conversationReference&quot;,&quot;name&quot;,&quot;updatedAt&quot;}]}`, newest first |
| `GET /shopping-assistant/conversations/messages` | Query parameter `conversationReference` | `{&quot;conversationReference&quot;,&quot;messages&quot;:[{&quot;role&quot;,&quot;content&quot;,&quot;attachments&quot;:[{&quot;name&quot;}],&quot;toolInvocations&quot;:[{&quot;name&quot;,&quot;result&quot;}]}]}` |
| `DELETE /shopping-assistant/conversations` | `X-CSRF-Token` header and a JSON body with `conversationReference` or `conversationReferences` | `{&quot;deletedCount&quot;: N}` |

&lt;/div&gt;

`toolInvocations[].result` has the same format as `output.result` in the stream, with two differences:

- The history returns only customer-facing tools.
- The product cards are refreshed with current data, but `refinementChips`, `activeFilters`, and `seeAll` are not included. They exist only in the live stream.

## Configuration

### Back Office settings

Storefront Assistant reads its settings from the **Storefront Assistant** tab of the `ai_commerce` feature in Configuration Management. All values are read on each request, so changes apply without a deployment.

&lt;div class=&quot;width-100&quot;&gt;

| SETTING KEY | TYPE | DESCRIPTION |
|-------------|------|-------------|
| `ai_commerce:storefront_assistant:general:is_enabled` | boolean | Enables the chat widget and the endpoints. |
| `ai_commerce:storefront_assistant:general:is_product_discovery_agent_enabled` | boolean | Enables the **Product Discovery** agent. |
| `ai_commerce:storefront_assistant:system_prompts:product_discovery_system_prompt` | text | The system prompt of the **Product Discovery** agent. A blank value uses the default prompt of the module. |
| `ai_commerce:storefront_assistant:ai_vendor:ai_configuration` | radio | The name of the AI configuration in `config_ai.php`. |
| `ai_commerce:storefront_assistant:ai_vendor:openai_model`, `aws_model`, `anthropic_model` | string | The model of each AI vendor. |
| `ai_commerce:storefront_assistant:suggested_prompts:*` | text | The suggested prompts for each page type and under the product cards. Separate the prompts with `\|`. To show titled sections, separate the sections with `\|\|` and start each section with its title and a colon. |

&lt;/div&gt;

The Client runs in the Yves process and reads only Storefront settings. Every setting that the Client or Yves reads must have `storefront: true`. The model settings must have `storefront: false`, because Zed resolves them.

### Client configuration

You can override the following methods in the Client `AiCommerceConfig`:

&lt;div class=&quot;width-100&quot;&gt;

| METHOD | DEFAULT | DESCRIPTION |
|--------|---------|-------------|
| `getStorefrontAssistantAiConfigurationName()` | `&apos;&apos;` | The name of the AI configuration for the turn. The project must override it. |
| `getProductDiscoveryAgentToolNames()` | `[]` | The tool names of the **Product Discovery** agent. The project must override it. |
| `getStorefrontAssistantCustomerFacingToolNames()` | `[]` | The tool names whose results go to the browser. The project must override it. |
| `getStorefrontAssistantSseAiConfigurationNames()` | The value of `getStorefrontAssistantAiConfigurationName()` | The AI configurations for which the SSE plugins send events. |
| `isStorefrontAssistantReasoningStreamed()` | `true` | Streams the reasoning of the model to the chat. |
| `getStorefrontAssistantConversationTimeToLive()` | `2592000` (30 days) | The TTL of each conversation that the customer can see, in seconds. |
| `getStorefrontAssistantConversationListLimit()` | `50` | The maximum number of conversations that the history list returns. |
| `getStorefrontAssistantConversationIndexLimit()` | `100` | The maximum number of conversation references in the index of a customer. When a new conversation exceeds the limit, the oldest reference is removed. Do not set it lower than the list limit. |
| `getStorefrontAssistantCategoryTreeLimit()` | `200` | The maximum number of categories that `category_tree` returns. |
| `getStorefrontAssistantInlineCategoryTreeLimit()` | `200` | The maximum number of category lines that the system prompt of the **Product Discovery** agent contains. A larger category tree is not added to the prompt, and the agent uses `category_tree` instead. |
| `getStorefrontAssistantMaxRefinementChips()` | `6` | The maximum number of refinement chips under the product cards. |
| `getStorefrontAssistantPriceOutlierFactor()` | `5.0` | A product price that is this many times higher than the median price of the other products with the same name is marked as unusual. |

&lt;/div&gt;

The attachment limits come from the Shared `AiCommerceConfig`: `getStorefrontAssistantSupportedAttachmentMimeTypes()`, `getStorefrontAssistantMaxAttachmentSizeBytes()`, and `getStorefrontAssistantMaxAttachmentCount()`.

### AI configuration

Storefront Assistant needs one AI configuration in `config/Shared/config_ai.php` for each AI vendor that you offer in the Back Office. The Client sends the name of the AI configuration to Zed with each turn. Zed accepts only the names that the Zed `AiFoundationConfig::getClientResolvableAiConfigurationNames()` returns. For the configuration examples and rules, see [Set up configuration](/docs/dg/dev/ai/ai-commerce/storefront-assistant/install-storefront-assistant.html#3-set-up-configuration).

## Frontend

The Storefront Assistant and the Back Office Assistant use one shared chat frontend: the `&lt;spryker-chat&gt;` custom element and its features, such as chat history, attachments, suggested prompts, and the agent selector. The module ships the shared chat in `vendor/spryker-feature/ai-commerce/src/SprykerFeature/Shared/AiCommerce/Theme/default/Chat/`.

The Storefront loads the shared chat as follows:

- `SharedThemeTwigLoaderPlugin` makes the shared Twig templates available as `@AiCommerce/Chat/...`.
- The `@shared/ai-commerce/*` alias of the Yves frontend builder resolves the shared TypeScript and SCSS files.
- The `storefront-assistant` organism embeds `@AiCommerce/Chat/core/chat.twig` with the Storefront URLs, limits, and texts. It loads the chat lazily into the `storefront-assistant` chunk.
- The Storefront features are ShopUi molecules in `vendor/spryker-feature/ai-commerce/src/SprykerFeature/Yves/AiCommerce/Theme/default/components/molecules/`: `chat-page-context`, `chat-tool-products`, `chat-tool-compare`, `chat-offer-choices`, and `chat-display-setup`.
- The `storefront-assistant-trigger` molecule renders a header button that opens and closes the chat.

Project files replace core files with the same path:

&lt;div class=&quot;width-100&quot;&gt;

| TO CHANGE | DO THIS |
|-----------|---------|
| A Storefront feature | Override the molecule in the project Yves theme and extend `molecule(&apos;&lt;molecule&gt;&apos;, &apos;@SprykerFeature:AiCommerce&apos;)`. To change only its settings, override the `data` of the molecule. |
| A shared chat template on all hosts | Put a Twig file with the same path in `src/Pyz/Shared/AiCommerce/Theme/default/Chat/`. To extend the core template instead of copying it, use the `@SprykerFeature:AiCommerce/Chat/...` path. |
| A shared chat feature script | Put an `index.ts` file with the same path in `src/Pyz/Shared/AiCommerce/Theme/default/Chat/features/&lt;feature&gt;/` and register a subclass of the core class. |

&lt;/div&gt;

The module README describes the blocks, the events, the style tokens, the tool renderer contract, and the other extension points of the shared chat: `vendor/spryker-feature/ai-commerce/src/SprykerFeature/Shared/AiCommerce/Theme/default/Chat/README.md`.

### Page context

The frontend reads the page context from the DOM and sends it with each prompt. This lets the agent know the current product, category, or search query.

The `chat-page-context` molecule defines the page context as a list of rules in its `data.rules`. The first rule that matches the page wins:

&lt;div class=&quot;width-100&quot;&gt;

| PAGE TYPE | MATCHES WHEN THE PAGE HAS | READS |
|-----------|---------------------------|-------|
| `search` | `.js-catalog__form` and a value in `input[name=&quot;q&quot;]` | The search query and the active filters |
| `category` | `.js-catalog__form` | The category name from `h1` and the active filters |
| `product` | `.product-detail` | The product name and SKU from `meta[itemprop=&quot;name&quot;]` and `meta[itemprop=&quot;abstractSku&quot;]` inside `[itemtype=&quot;https://schema.org/Product&quot;]` |
| `category` | `[itemtype=&quot;https://schema.org/Product&quot;]` without `.js-catalog__form` and `.product-detail`, with at least three URL path segments | The category name from `h1` |

&lt;/div&gt;

The active filters are the checked checkboxes and radio buttons in `.js-catalog__form`.

If your theme changes this markup, the page context and the page-specific suggested prompts do not work. In this case, override the `chat-page-context` molecule at the project level and change its `data.rules`. The project list replaces the core list, so list all rules that you need.

### Theming

The chat takes all colors, fonts, and sizes from `--chat-*` CSS custom properties. The `storefront-assistant` organism maps these properties to the `ShopUi` theme, so the chat uses the brand color of the shop automatically. The panel width is `clamp(400px, 28vw, 480px)`. On small screens, the panel uses the full width.

To change the chat, set the properties in a project stylesheet for both selectors. The trigger button is outside the chat element, so it needs the second selector:

```scss
storefront-assistant spryker-chat,
.chat-trigger {
    --chat-color-accent: #{$setting-color-main};
    --chat-size-panel-width: clamp(400px, 28vw, 480px);
}
```

{% info_block warningBox &quot;Brand color&quot; %}

Do not use `rgba()` or `color.adjust()` on `$setting-color-main`. This value is a CSS custom property, not a Sass color. Use `helper-color-light()` or `helper-color-dark()` instead.

{% endinfo_block %}

When the panel is open, the chat changes the page layout with these attributes on the `&lt;html&gt;` element:

&lt;div class=&quot;width-100&quot;&gt;

| ATTRIBUTE | EFFECT |
|-----------|--------|
| `data-chat-docked` | The panel is open and narrower than the screen. The `body` gets a right padding equal to the `--chat-dock-width` custom property, so the page moves to the left. |
| `data-chat-expanded` | The panel uses the full width, and the page does not scroll. |

&lt;/div&gt;

The `body` padding does not move elements with `position: fixed` or `position: sticky`, such as a sticky header or a cookie banner. If such elements cover the panel, add `right: var(--chat-dock-width)` to them under `:root[data-chat-docked]`.

## Extension points

&lt;div class=&quot;width-100&quot;&gt;

| INTERFACE | STACK | PURPOSE |
|-----------|-------|---------|
| `SprykerFeature\Client\AiCommerce\Dependency\StorefrontAssistant\StorefrontAssistantAgentPluginInterface` | Client `AiCommerceDependencyProvider::getStorefrontAssistantAgentPlugins()` | Adds an agent. The order of the stack sets the priority in **Auto** mode. |
| `SprykerFeature\Client\AiCommerce\Dependency\StorefrontAssistant\StorefrontAssistantProductCollectionExpanderPluginInterface` | Client `AiCommerceDependencyProvider::getStorefrontAssistantProductCollectionExpanderPlugins()` | Adds data to the product cards. A plugin must not throw an exception, because an exception stops the turn. |
| `Spryker\Client\AiFoundation\Dependency\Tools\ToolPluginInterface` | Client `AiFoundationDependencyProvider::getAiToolPlugins()` | Adds a tool. Also add the tool name to the tool list of an agent. |
| `Spryker\Client\SearchExtension\Dependency\Plugin\SearchConfigExpanderPluginInterface` | Client `AiCommerceDependencyProvider::getSearchConfigExpanderPlugins()` | Adds facets to `catalog_search`. |

&lt;/div&gt;

## Install

[Install Storefront Assistant](/docs/dg/dev/ai/ai-commerce/storefront-assistant/install-storefront-assistant.html)
</description>
            <pubDate>Thu, 29 Oct 2026 09:26:36 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/ai/ai-commerce/storefront-assistant/storefront-assistant.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/ai/ai-commerce/storefront-assistant/storefront-assistant.html</guid>
            
            
        </item>
        
    </channel>
</rss>
