<?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, 05 Oct 2026 14:26:44 +0000</lastBuildDate>
        <generator>Jekyll v4.2.2</generator>
        
        
        <item>
            <title>Integrate Symfony Scheduler</title>
            <description>This document describes how to integrate and configure Symfony Scheduler module into a Spryker project.

## Install

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

The Symfony Scheduler module requires:

- PHP 8.3 or higher.
- `symfony/scheduler` `^6.4` or `^7.4`.
- The `ext-pcntl` PHP extension, if you want parallel workers to shut their subprocesses down on a signal.
- An AMQP broker, if you run scheduled jobs asynchronously (see [Run scheduled jobs asynchronously](#run-scheduled-jobs-asynchronously)). The synchronous setup needs no broker.

{% endinfo_block %}

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

Check if the following modules have been installed. Installing `spryker/symfony-scheduler` with Composer pulls the dependencies (`Lock`, `Redis`, `Gui`, `SymfonyMessengerExtension`) in automatically:

| MODULE                    | EXPECTED DIRECTORY                         |
|---------------------------|--------------------------------------------|
| SymfonyMessenger          | vendor/spryker/symfony-messenger           |
| SymfonyMessengerExtension | vendor/spryker/symfony-messenger-extension |
| SymfonyScheduler          | vendor/spryker/symfony-scheduler           |
| SymfonySchedulerExtension | vendor/spryker/symfony-scheduler-extension |
| Lock                      | vendor/spryker/lock                        |
| Redis                     | vendor/spryker/redis                       |
| Gui                       | vendor/spryker/gui                         |

The `Lock` module guards each scheduled job against concurrent execution, `Redis` backs the job status storage and the Back Office controls, and `Gui` provides the Back Office **Scheduler** page.

If modules are present, proceed to the next step. If not, install the missing modules using Composer before proceeding.

{% endinfo_block %}

Install the required modules using Composer:

```shell
composer require spryker/symfony-scheduler spryker/symfony-messenger
```

## Configure

To configure the Symfony Scheduler module, you need to define your scheduled tasks and their execution intervals.
With the current implementation you can add them in the module config or provide them via a plugin.

### Configure via Module Config

If you want to execute some console commands on a cron schedule, you can define them in the `getCronJobs` method of the `SymfonySchedulerConfig` class.
For each job a transport is created in the Symfony Messenger module under the name of the job, and a handler runs the command in a subprocess. Which handler plugin you register decides *where* that subprocess runs:

| PLUGIN | BEHAVIOR |
|---|---|
| `AsyncCompiledCronTransportsHandlerProviderPlugin` | Queues the due job for a separate worker to run. Recommended — see [Run scheduled jobs asynchronously](#run-scheduled-jobs-asynchronously). |
| `CompiledCronTransportsHandlerProviderPlugin` | Runs the due job inside the polling loop. Deprecated, and the default until you configure the asynchronous setup. |

The rest of this section applies to both.

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

```php
&lt;?php

namespace Pyz\Client\SymfonyScheduler;

use Spryker\Client\SymfonyScheduler\SymfonySchedulerConfig as SprykerSymfonySchedulerConfigAlias;
use Spryker\Shared\MessageBroker\MessageBrokerConstants;

class SymfonySchedulerConfig extends SprykerSymfonySchedulerConfigAlias
{
    public function getCronJobs(): array
    {
        $jobs = [
            &apos;queue-worker-start&apos; =&gt; [
                &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console queue:worker:start&apos;,
                &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
            ],
            &apos;check-oms-conditions&apos; =&gt; [
                &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console oms:check-condition&apos;,
                &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
            ],
            &apos;check-oms-timeouts&apos; =&gt; [
                &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console oms:check-timeout&apos;,
                &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
            ],
            &apos;clear-oms-locks&apos; =&gt; [
                &apos;command&apos; =&gt; &apos;$PHP_BIN vendor/bin/console oms:clear-locks&apos;,
                &apos;schedule&apos; =&gt; &apos;0 6 * * *&apos;,
                &apos;priority&apos; =&gt; 100,
            ],
        ];

        return $jobs;
    }
}
```

The job name is an unique key of job definition and it will be used as a transport name in the Symfony Messenger module.
The `command` is the console command that you want to execute.
The `schedule` is the cron expression that defines when the job should be executed. You can also use aliases like `@hourly`, `@daily`, etc.
The `no_lock` option is optional and defines whether the job runs without acquiring a lock. By default (`no_lock` omitted or `false`), the job is guarded in two places: its schedule is locked, so only one worker advances it and emits a given occurrence, and its execution is locked, so the command never runs twice at the same time. Setting `no_lock` to `true` removes both, which lets several workers run the job concurrently. Only set it for jobs that are idempotent or otherwise safe to run concurrently.
The `priority` option is optional and defines the consumption order of the job&apos;s transport by the worker. The higher the number, the earlier the job is polled. When omitted, priority defaults to `0`.
In addition you can also provide a `store` or a `region`, which works in the same way as originally in `jenkins.php`

### Configure via new plugin

If your use case is more complex than just executing a console command, you can create a new plugin that implements `\Spryker\Shared\SymfonySchedulerExtension\Dependency\Plugin\SchedulerHandlerProviderPluginInterface`.
In example below you can see that we define messages, handlers and schedules for the job.
Messages and handlers are concepts from the Symfony Messenger module. They will be used to trigger and process the job. Schedules are the expressions that define when the job should be executed. It can be simple cron expression or a more complex one like callback trigger, combination of multiple triggers or even a custom trigger that you can implement by yourself.
You don&apos;t need to map messages and handlers separately in the SymfonyMessenger module, because the SymfonyScheduler module will take care of things like defining the transport and mapping the message to the handler for the transport.

```php
&lt;?php

namespace Pyz\Client\FooBar\Plugin\SymfonyScheduler;

use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;

class FooBarSchedulerHandlerProviderPlugin implements SchedulerHandlerProviderPluginInterface
{
    public function getHandlers(): array
    {
        return [
            RecurringReportGenerationMessage::class =&gt; [ //You can have multiple handlers for the same message
                new ReportGenerationHandler(),
            ],
        ];
    }
    public function getSchedules(): array
    {
        $schedule = new Schedule();
        $schedule-&gt;add(RecurringMessage::cron(&apos;* * * * *&apos;), (new RecurringReportGenerationMessage(&apos;report for today&apos;))) // every day at midnight

        return [
            &apos;report-generation&apos; =&gt; $schedule // transport name will be &quot;report-generation&quot;
        ];
    }
}
```

## Wiring plugins

If you define your jobs with the first option (via config) or with separate plugin, you need to wire plugins in the Symfony Scheduler Dependency Provider by adding the following code:

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

```php
&lt;?php

namespace Pyz\Client\SymfonyScheduler;

use Spryker\Client\SymfonyScheduler\Plugin\SymfonyScheduler\AsyncCompiledCronTransportsHandlerProviderPlugin;
use Spryker\Client\SymfonyScheduler\SymfonySchedulerDependencyProvider as SprykerSymfonySchedulerDependencyProvider;

class SymfonySchedulerDependencyProvider extends SprykerSymfonySchedulerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonySchedulerExtension\Dependency\Plugin\SchedulerHandlerProviderPluginInterface&gt;
     */
    protected function getSchedulerHandlerProviderPlugins(): array
    {
        return [
            new AsyncCompiledCronTransportsHandlerProviderPlugin(), //Plugin that provides handlers for jobs defined in the config
            new FooBarSchedulerHandlerProviderPlugin(), //Plugin that provides scheduled jobs separately
        ];
    }
}
```

`AsyncCompiledCronTransportsHandlerProviderPlugin` queues each due job instead of running it in the polling loop, so a long-running job cannot hold up the jobs behind it. It needs the asynchronous execution tier to be configured; until that is in place, register the deprecated `CompiledCronTransportsHandlerProviderPlugin` instead, which runs jobs inline. See [Run scheduled jobs asynchronously](#run-scheduled-jobs-asynchronously).

## Register the scheduler transports with Symfony Messenger

Scheduled jobs are executed through the Symfony Messenger worker, so the scheduler ships a set of plugins that register its transports, message-to-handler mapping, transport factory, and the Back Office control plugins with the Symfony Messenger module. Wire them in the Symfony Messenger dependency provider.

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

use Spryker\Client\SymfonyMessenger\SymfonyMessengerDependencyProvider as SprykerSymfonyMessengerDependencyProvider;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\AsyncSchedulerJobMessageMappingProviderPlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\AsyncSchedulerJobTransportConfigProviderPlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\AsyncSchedulerJobTransportFactoryProviderPlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\CompiledCronTransportGroupAwarePlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\SchedulerAvailableTransportConfigProviderPlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\SchedulerMessageMappingProviderPlugin;
use Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger\SchedulerTransportFactoryProviderPlugin;

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportFactoryProviderPluginInterface&gt;
     */
    protected function getTransportFactoryProviderPlugins(): array
    {
        return [
            new SchedulerTransportFactoryProviderPlugin(),
            new AsyncSchedulerJobTransportFactoryProviderPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface&gt;
     */
    protected function getAvailableTransportConfigProviderPlugins(): array
    {
        return [
            new SchedulerAvailableTransportConfigProviderPlugin(),
            new AsyncSchedulerJobTransportConfigProviderPlugin(),
        ];
    }

    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\MessageMappingProviderPluginInterface&gt;
     */
    protected function getMessageMappingProviderPlugins(): array
    {
        return [
            new SchedulerMessageMappingProviderPlugin(),
            new AsyncSchedulerJobMessageMappingProviderPlugin(),
        ];
    }

    protected function getGroupAwareTransportsPlugins(): array
    {
        return [
            new CompiledCronTransportGroupAwarePlugin(),
        ];
    }

}
```

Plugins for the schedules themselves:

- `SchedulerAvailableTransportConfigProviderPlugin` registers one Messenger transport per scheduled job, together with the job&apos;s `priority`. It supersedes the deprecated `SchedulerAvailableTransportProviderPlugin`.
- `SchedulerTransportFactoryProviderPlugin`, `SchedulerMessageMappingProviderPlugin`, and `CompiledCronTransportGroupAwarePlugin` provide the scheduler transport factory, the message-to-handler mapping, and the transport grouping respectively.

Plugins for the asynchronous execution queue. Register them only when you run jobs asynchronously; they are listed above so the dependency provider is complete in one place:

- `AsyncSchedulerJobTransportFactoryProviderPlugin` provides the factory that builds the job queue&apos;s transport.
- `AsyncSchedulerJobTransportConfigProviderPlugin` registers that queue as a receiver you can name on the command line.
- `AsyncSchedulerJobMessageMappingProviderPlugin` routes a queued job to the handler that runs it under a per-job lock.

{% info_block infoBox &quot;Disabling a job no longer pauses its transport&quot; %}

Earlier versions registered `DisabledSchedulerJobTransportGuardPlugin` under `getTransportConsumeGuardPlugins()` to skip a disabled job&apos;s transport. That plugin is deprecated and must no longer be wired: pausing a transport stops its schedule from advancing, which freezes the stored schedule state and makes re-enabling the job replay every occurrence it missed while disabled. The disabled state is now applied by the handler, so the schedule keeps advancing and the job is simply not run. Remove the plugin from `getTransportConsumeGuardPlugins()` when you upgrade.

{% endinfo_block %}

## Run scheduled jobs asynchronously

By default a due job is executed by the same loop that polls the schedules, so that loop is blocked until the command finishes. A job that takes longer than its interval therefore delays every job behind it, and the jobs with the lowest priority can be delayed indefinitely.

Running jobs asynchronously separates the two responsibilities:

- A **trigger** worker polls the schedules and publishes each due job to a queue. It never runs a command, so its loop stays free.
- One or more **executor** workers consume that queue and run the commands.

### Configure the queue connection

The scheduler publishes to and consumes from its own AMQP connection, configured under its own constants. Point it at the broker the project already runs:

**config/Shared/config_default.php**

```php
&lt;?php

use Spryker\Shared\SymfonyMessenger\SymfonyMessengerConstants;
use Spryker\Shared\SymfonyScheduler\SymfonySchedulerConstants;

// Reuses the broker the Symfony Messenger queue transport already points at.
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_AMQP_HOST] = $config[SymfonyMessengerConstants::QUEUE_AMQP_HOST];
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_AMQP_PORT] = $config[SymfonyMessengerConstants::QUEUE_AMQP_PORT];
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_AMQP_USERNAME] = $config[SymfonyMessengerConstants::QUEUE_AMQP_USERNAME];
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_AMQP_PASSWORD] = $config[SymfonyMessengerConstants::QUEUE_AMQP_PASSWORD];
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_AMQP_VIRTUAL_HOST] = $config[SymfonyMessengerConstants::QUEUE_AMQP_VIRTUAL_HOST];
```

| CONSTANT | DESCRIPTION |
|---|---|
| `SYMFONY_SCHEDULER_AMQP_HOST` | Broker host. |
| `SYMFONY_SCHEDULER_AMQP_PORT` | Broker port. Defaults to `5672`. |
| `SYMFONY_SCHEDULER_AMQP_USERNAME` | Username. Defaults to `spryker`. |
| `SYMFONY_SCHEDULER_AMQP_PASSWORD` | Password. |
| `SYMFONY_SCHEDULER_AMQP_VIRTUAL_HOST` | Virtual host. Defaults to `/`. |
| `SYMFONY_SCHEDULER_AMQP_DSN` | A complete DSN, used when `SYMFONY_SCHEDULER_AMQP_HOST` is not set. |

The queue name and its transport options come from `\Spryker\Client\SymfonyScheduler\SymfonySchedulerConfig` and can be overridden at the project level:

| METHOD | DEFAULT | DESCRIPTION |
|---|---|---|
| `getJobTransportName()` | `scheduler-jobs` | Name of the queue, and the receiver name you pass to the consumer. |
| `getJobTransportDsn()` | `scheduler-amqp://scheduler-jobs` | DSN of the queue transport. The scheme is owned by the scheduler&apos;s own transport factory. |
| `getJobQueueTransportOptions(string $queueName)` | `auto_setup`, exchange and queue declaration | AMQP options for the queue. |
| `getJobExecutionLockTtl()` | `360.0` | Seconds a job&apos;s execution lock is held. Covers the handler&apos;s 300-second command timeout with a margin. |

{% info_block infoBox &quot;The scheduler builds this transport itself&quot; %}

The queue transport is created by the scheduler rather than by the shared Symfony Messenger transport creation, so it is unaffected by the serializer and transport options that apply to the queue adapter. This is why it uses a scheme of its own (`scheduler-amqp://`) and why its connection is configured separately from `SymfonyMessengerConstants::QUEUE_*`.

{% endinfo_block %}

### Register the plugins

Register `AsyncCompiledCronTransportsHandlerProviderPlugin` in the Symfony Scheduler dependency provider (see [Wiring plugins](#wiring-plugins)) and the three `AsyncSchedulerJob*` plugins in the Symfony Messenger dependency provider (see [Register the scheduler transports with Symfony Messenger](#register-the-scheduler-transports-with-symfony-messenger)).

### Run both tiers

Both tiers are the same consumer command with different receivers. Use `--worker-receivers` to dedicate one child process to the schedules, and `--parallel` to add executors:

```shell
vendor/bin/console symfonymessenger:consume scheduler-jobs --worker-receivers=compiled-cron-scheduler --parallel=3
```

This runs four processes: one trigger consuming `compiled-cron-scheduler`, and three executors consuming `scheduler-jobs`. Raising `--parallel` adds executors only. See [Consume transports in parallel](/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger.html#consume-transports-in-parallel).

{% info_block warningBox &quot;Run exactly one trigger&quot; %}

Give the schedules a single dedicated child. Additional trigger processes do not share the polling work: each schedule is guarded by a lock that the holding worker keeps for its lifetime, so the jobs are split across the processes rather than balanced, and a process that loses every race sits idle.

{% endinfo_block %}

### How a job is prevented from running twice

Each job is locked for the duration of its command, under `scheduler:job:running:&lt;name&gt;`. A second copy of the same job — whether from an extra trigger or from the broker redelivering a message whose connection dropped — finds the lock held, is discarded, and is logged. The schedule produces the job again at its next due time.

A job configured with `no_lock` set to `true` is exempt and can run concurrently with itself.

{% info_block infoBox &quot;If the lock store is unreachable&quot; %}

Single execution cannot be guaranteed without the lock, so the job is skipped and an error is logged rather than run unguarded. The queue keeps draining, and the next due occurrence is unaffected.

{% endinfo_block %}

## Keep schedule state across restarts

Consumers are recycled regularly — for example by `--time-limit`. Each schedule records how far it has progressed in Redis, so a new process resumes from that point instead of restarting at the current time. Without it, every occurrence that fell due during a restart is skipped.

The state is stored through the scheduler&apos;s own Redis connection (see [Configure the job status storage](#configure-the-job-status-storage)):

| METHOD | DEFAULT | DESCRIPTION |
|---|---|---|
| `getScheduleStateStorageKeyPrefix()` | `scheduler:state:` | Prefix for the per-job schedule state. |

{% info_block warningBox &quot;Occurrences missed during an outage are replayed&quot; %}

Because the position is remembered, a trigger that starts after an outage publishes every occurrence it missed, not just the next one. For a job that runs every minute, an hour of downtime means roughly sixty queued runs. Keep this in mind for jobs that are expensive or that act on a backlog, and consider `no_lock` and interval choices accordingly.

{% endinfo_block %}

If the state cannot be reached, the scheduler logs a warning and falls back to per-process state: it keeps running, but each restart resets the schedule to the current time.

## Configure the job status storage

The Back Office scheduler page and the enable/disable and run-now controls persist their state (job statuses, disabled markers, and run requests) in Redis, through a connection the module configures under its own `SymfonySchedulerConstants` keys. This is a separate connection *configuration*, not a separate Redis instance: by default it resolves to the same key-value store Redis (same host, port, and database) as the storage, and the scheduler&apos;s own key prefixes (`scheduler:job:*`) keep its entries distinct from storage keys. Configure the connection in `config/Shared/config_default.php`:

**config/Shared/config_default.php**

```php
&lt;?php

use Spryker\Shared\SymfonyScheduler\SymfonySchedulerConstants;

// &gt;&gt;&gt; SYMFONY SCHEDULER

$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_PERSISTENT_CONNECTION] = true;
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_SCHEME] = getenv(&apos;SPRYKER_KEY_VALUE_STORE_PROTOCOL&apos;) ?: &apos;tcp&apos;;
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_HOST] = getenv(&apos;SPRYKER_KEY_VALUE_STORE_HOST&apos;);
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_PORT] = getenv(&apos;SPRYKER_KEY_VALUE_STORE_PORT&apos;);
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_USER] = getenv(&apos;SPRYKER_KEY_VALUE_USERNAME&apos;);
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_PASSWORD] = getenv(&apos;SPRYKER_KEY_VALUE_PASSWORD&apos;);
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_DATABASE] = $keyValueRegionNamespaces[$namespaceKey][&apos;namespace&apos;] ?? getenv(&apos;SPRYKER_KEY_VALUE_STORE_NAMESPACE&apos;) ?: 1;
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_DATA_SOURCE_NAMES] = json_decode(getenv(&apos;SPRYKER_KEY_VALUE_STORE_SOURCE_NAMES&apos;) ?: &apos;[]&apos;, true) ?: [];
$config[SymfonySchedulerConstants::SYMFONY_SCHEDULER_REDIS_CONNECTION_OPTIONS] = json_decode(getenv(&apos;SPRYKER_KEY_VALUE_STORE_CONNECTION_OPTIONS&apos;) ?: &apos;[]&apos;, true) ?: [];
```

The following `SymfonySchedulerConstants` keys are available:

| CONSTANT                                         | DESCRIPTION                                                             |
|--------------------------------------------------|-------------------------------------------------------------------------|
| `SYMFONY_SCHEDULER_REDIS_SCHEME`                 | Connection scheme/protocol, for example `tcp` or `redis`.               |
| `SYMFONY_SCHEDULER_REDIS_HOST`                   | Redis host.                                                             |
| `SYMFONY_SCHEDULER_REDIS_PORT`                   | Redis port.                                                             |
| `SYMFONY_SCHEDULER_REDIS_DATABASE`               | Redis database index.                                                   |
| `SYMFONY_SCHEDULER_REDIS_USER`                   | Username for Redis ACL authentication.                                  |
| `SYMFONY_SCHEDULER_REDIS_PASSWORD`               | Password.                                                               |
| `SYMFONY_SCHEDULER_REDIS_PERSISTENT_CONNECTION`  | Whether to use a persistent connection.                                 |
| `SYMFONY_SCHEDULER_REDIS_DATA_SOURCE_NAMES`      | Array of DSN strings for a cluster/replication setup.                   |
| `SYMFONY_SCHEDULER_REDIS_CONNECTION_OPTIONS`     | Array of connection options passed to the Redis client.                 |

The storage key prefixes and their time-to-live are defined in `\Spryker\Client\SymfonyScheduler\SymfonySchedulerConfig` and can be overridden at the project level:

| METHOD                              | DEFAULT                    | DESCRIPTION                                                                         |
|-------------------------------------|----------------------------|-------------------------------------------------------------------------------------|
| `getJobStatusStorageKeyPrefix()`    | `scheduler:job:status:`    | Prefix for per-job status records.                                                  |
| `getJobStatusTtl()`                 | `86400` (24 h)             | TTL of a status record; stale entries from a crashed worker auto-expire.            |
| `getJobDisabledStorageKeyPrefix()`  | `scheduler:job:disabled:`  | Prefix for the disabled marker. A job is disabled while its marker key exists (no TTL). |
| `getJobRunRequestStorageKeyPrefix()`| `scheduler:job:run:`       | Prefix for the on-demand run-request marker.                                        |
| `getJobRunRequestTtl()`             | `300` (5 min)              | TTL of a run request; a request that is never consumed auto-expires.                |

## Run the Scheduler

To run a scheduler you need to run the SymfonyMessenger consumer with the transport name that is configured for the job.

```shell
vendor/bin/console symfonymessenger:consume queue-worker-start report-generation
```

This runs the schedules and the jobs in one process. To stop a long-running job from delaying the others, run the jobs asynchronously instead — see [Run scheduled jobs asynchronously](#run-scheduled-jobs-asynchronously).

{% info_block warningBox &quot;--parallel does not spread scheduler transports&quot; %}

Adding `--parallel` to a command that consumes `schedule://` transports does not share the polling work. Each schedule is guarded by a lock that the worker holding it keeps for its lifetime, so the jobs are statically split across the child processes rather than balanced, and a child that loses every race does nothing. Use `--parallel` for the execution queue, where the processes are genuine competing consumers.

{% endinfo_block %}

Jobs configured with `no_lock` set to `true` are not guarded, so they can be executed concurrently. Use `no_lock` only for jobs that are safe to run that way (see [Configure via Module Config](#configure-via-module-config)).

## Monitor and control scheduled jobs in the Back Office

The module adds a **Scheduler** page in the Back Office under **Maintenance &gt; Scheduler**. It lists every scheduled job in a live table (refreshed every 5 seconds) with the following columns: Name, Command, Schedule, Priority, Status, Started, Finished, Duration, and Actions.

The **Status** reflects the latest recorded execution state of each job:

| STATUS   | MEANING                                                                       |
|----------|-------------------------------------------------------------------------------|
| Waiting  | The job is registered and awaiting its next run.                              |
| Queued   | The job is due and has been published to the execution queue, but no executor has picked it up yet. Only occurs in the asynchronous setup. |
| Running  | The job is currently being executed by a worker.                              |
| Success  | The last execution finished successfully.                                     |
| Error    | The last execution failed; the captured output is available on the job&apos;s detail page. |
| Disabled | The job is disabled and will not be consumed until it is enabled again.       |

Statuses are recorded while a job is queued and while it runs, and are stored through the scheduler&apos;s Redis connection configuration (see [Configure the job status storage](#configure-the-job-status-storage)). `Disabled` is derived from the disabled marker and is never persisted as a status record.

The **Duration** column measures execution only. Time spent waiting in the queue is not counted, so a job that sits in `Queued` shows no duration until an executor starts it.

Each row provides the following actions:

- **View** — opens a detail page with the job&apos;s name, command, status, start/finish timestamps, and error message (if any).
- **Enable / Disable** — toggles the job. Disabling writes a marker to Redis, and the job is no longer run until it is enabled again. Its schedule keeps advancing while it is disabled, so enabling resumes from the current time rather than replaying the occurrences it missed. In the asynchronous setup the marker is checked twice: once before a job is queued, and again before a queued job is run, so disabling also stops a job that is already waiting in the queue.
- **Run now** — requests an immediate, one-off execution ahead of the cron schedule. A run-request marker is written to Redis; the scheduler&apos;s run-request-aware trigger consumes it on the next poll and fires the job exactly once. In the asynchronous setup this queues the job within a poll rather than running it there and then, so it starts as soon as an executor is free. The action is unavailable while a job is disabled or already running.

{% info_block infoBox &quot;Requires the job status storage&quot; %}

The Back Office page and the enable/disable and run-now controls require the scheduler&apos;s Redis connection to be configured. If Redis is unavailable, the controls fail open — a job is treated as enabled and no forced run is scheduled — so a Redis outage never pauses every job.

{% endinfo_block %}

## How it works

The Back Office controls add two decision points to this flow:

- A disabled job is skipped by the handler rather than by pausing its transport, so its schedule keeps advancing and the stored position stays current. In the asynchronous setup the check runs again before a queued job is executed, which also covers a job disabled after it was queued.
- When a **Run now** request exists for a job, the run-request-aware trigger returns the current time as the next run date, pre-empting the cron schedule so the message is yielded immediately. The request marker is consumed atomically, guaranteeing exactly one extra run.

## Running consumer as a background process

In order to run the consumer you can use a Jenkins in order to run it or any other manager like Stable Workers.

Jenkins example:

**config/Zed/cronjobs/jenkins.php**

```php
&lt;?php

$jobs[] = [
        &apos;name&apos; =&gt; &apos;consume-queue&apos;,
        &apos;command&apos; =&gt; $logger . &apos;$PHP_BIN vendor/bin/console symfonymessenger:consume queue-worker-start --time-limit=3600&apos;,
        &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
        &apos;enable&apos; =&gt; true,
    ];
    $jobs[] = [
        &apos;name&apos; =&gt; &apos;consume-other-jobs&apos;,
        &apos;command&apos; =&gt; $logger . &apos;$PHP_BIN vendor/bin/console symfonymessenger:consume scheduler-jobs&apos;
            . &apos; --worker-receivers=compiled-cron-scheduler&apos;
            . &apos; --parallel=3&apos;
            . &apos; --time-limit=3600&apos;,
        &apos;schedule&apos; =&gt; &apos;* * * * *&apos;,
        &apos;enable&apos; =&gt; true,
    ];

if (getenv(&apos;SPRYKER_CURRENT_REGION&apos;)) {
    foreach ($jobs as $job) {
        $job[&apos;region&apos;] = getenv(&apos;SPRYKER_CURRENT_REGION&apos;);
    }
}
```

As you see we defined 2 Jenkins jobs. The first consumes the queue worker messages: that process runs for at least a minute, so giving it its own consumer keeps it from delaying everything else.

The second covers the remaining cron jobs and spawns four processes of its own — one trigger on `compiled-cron-scheduler` and three executors on `scheduler-jobs`. To add executor capacity, raise `--parallel` rather than defining more Jenkins jobs. If you have not configured the asynchronous setup, drop `scheduler-jobs` and `--worker-receivers` and consume `compiled-cron-scheduler` directly.

{% info_block infoBox &quot;Dedicated and shared workers are additive&quot; %}

`--worker-receivers` adds a process on top of the `--parallel` ones rather than taking one of them, so the command above runs `1 + 3` processes. Raising `--parallel` by one adds one executor and leaves the trigger alone.

{% endinfo_block %}
{% info_block warningBox &quot;Important&quot; %}
Jenkins by default has 2 executors so both of those jobs will be running in parallel. It&apos;s not possible to use this setup with 1 executor as the second job will never start because the first one will be running all the time.
{% endinfo_block %}</description>
            <pubDate>Mon, 05 Oct 2026 12:12:07 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler.html</guid>
            
            
        </item>
        
        <item>
            <title>Integrate Symfony Messenger</title>
            <description>This document describes how to integrate and configure the Symfony Messenger module into your Spryker project.

## Description

Symfony Messenger is a component that lets you dispatch and handle messages using different transports. By integrating Symfony Messenger into your Spryker project, you can switch between RabbitMQ and other transports, such as SQS, Redis, and even a database, for queue handling. You can also use Symfony Messenger for other use cases that require synchronous or asynchronous message processing.

{% info_block warningBox &quot;Check if you are using correct mode&quot; %}

Symfony Messenger module working only with Dynamic Multistore mode, so make sure that your project is using it before proceeding with the installation and configuration.

{% endinfo_block %}

## Install

{% info_block warningBox &quot;Check if you have it installed&quot; %}

Check that the following modules have been installed:

| MODULE                    | EXPECTED DIRECTORY                         |
|---------------------------|--------------------------------------------|
| SymfonyMessenger          | vendor/spryker/symfony-messenger           |
| SymfonyMessengerExtension | vendor/spryker/symfony-messenger-extension |

If so, skip this section. If not, install the missing modules before proceeding.

{% endinfo_block %}

Install the required modules using Composer:

```shell
composer require spryker/symfony-messenger
```

## Usage as a Queue Adapter

You can use Symfony Messenger as a queue adapter in Spryker to replace the existing RabbitMQ adapter.

In order to use Symfony Messenger as a queue adapter, you need to configure it and enable the corresponding plugins.

### Configure

1. Provide a DSN for the Queue transport in `config/Shared/config_default.php`:

```php
&lt;?php

use Spryker\Shared\SymfonyMessenger\SymfonyMessengerConstants;

// Symfony Messenger configuration
$config[SymfonyMessengerConstants::QUEUE_DSN] = &apos;amqp://guest:guest@localhost:5672/eu_host&apos;
];
```

Or you can build it with RabbitMQ connection details:

***config/Shared/config_default.php***

```php
foreach ($rabbitConnections as $key =&gt; $connection) {
    ...
    $config[SymfonyMessengerConstants::QUEUE_DSN] = sprintf(
        &apos;amqp://%s:%s@%s:%s/%s&apos;,
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_USERNAME],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_PASSWORD],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_HOST],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_PORT],
        $config[RabbitMqEnv::RABBITMQ_CONNECTIONS][$key][RabbitMqEnv::RABBITMQ_VIRTUAL_HOST],
    );
}
```

The protocol in the DSN determines which transport is used. Out of the box, Spryker provides RabbitMQ as the transport for queue processing. You do not need to provide a queue name in the DSN because the application defines it when dispatching messages.

2. Provide a list of queues that can be processed.

***src/Pyz/Client/SymfonyMessenger/SymfonyMessengerConfig.php***

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerConfig extends SprykerSymfonyMessengerConfig
{
    /**
     * @return array&lt;mixed&gt;
     */
    public function getQueueConfiguration(): array
    {
        return array_merge(
            [
                EventConstants::EVENT_QUEUE =&gt; [
                    EventConfig::EVENT_ROUTING_KEY_RETRY =&gt; EventConstants::EVENT_QUEUE_RETRY,
                    EventConfig::EVENT_ROUTING_KEY_ERROR =&gt; EventConstants::EVENT_QUEUE_ERROR,
                ],
                ...
            ],
        );
    }
}
```

This configuration is similar to the RabbitMQ configuration, so you can copy it from `\Pyz\Client\RabbitMq\RabbitMqConfig::getQueueConfiguration()`.

### Enable Queue Adapter

To enable the Symfony Messenger queue adapter, register the required plugins:

1. Add the Symfony Messenger transport plugin to `src/Pyz/Zed/Queue/QueueDependencyProvider.php`:

***src/Pyz/Zed/Queue/QueueDependencyProvider.php***

```php
&lt;?php

namespace Pyz\Client\Queue;

use Spryker\Client\Kernel\Container;
use Spryker\Client\Queue\QueueDependencyProvider as BaseQueueDependencyProvider;

class QueueDependencyProvider extends BaseQueueDependencyProvider
{
    /**
     * @param \Spryker\Client\Kernel\Container $container
     *
     * @return array&lt;\Spryker\Client\Queue\Model\Adapter\AdapterInterface&gt;
     */
    protected function createQueueAdapters(Container $container): array
    {
        return [
            $container-&gt;getLocator()-&gt;rabbitMq()-&gt;client()-&gt;createQueueAdapter(),
            // You can add the adapter from the Symfony Messenger module without removing the existing one so that you can switch between them when needed.
            $container-&gt;getLocator()-&gt;symfonyMessenger()-&gt;client()-&gt;createQueueAdapter(),
        ];
    }
}
```

2. Enable adapter in `config/Shared/config_default.php`:

***config/Shared/config_default.php***

```php
&lt;?php

use Spryker\Client\SymfonyMessenger\Adapter\SymfonyMessengerQueueAdapter;

$config[QueueConstants::QUEUE_ADAPTER_CONFIGURATION] = [
    EventConstants::EVENT_QUEUE =&gt; [
        QueueConfig::CONFIG_QUEUE_ADAPTER =&gt; SymfonyMessengerQueueAdapter::class,
    ],
];

$config[QueueConstants::QUEUE_ADAPTER_CONFIGURATION_DEFAULT] = [
    QueueConfig::CONFIG_QUEUE_ADAPTER =&gt; SymfonyMessengerQueueAdapter::class,
];
```

This steps will replace the existing RabbitMQ adapter with the Symfony Messenger adapter for the queues defined in the configuration.

### Additional configuration

To provide additional configuration for the Symfony Messenger transport, use the following approach:

#### Provide queue transport configuration

You can specify transport options per queue or provide a default configuration for all queues.

The following example shows the default configuration for all queues.
Example below is a default configuration for the AMQP transport, which is used for queue processing in Symfony Messenger. You can adjust it according to your needs.

***src/Pyz/Client/SymfonyMessenger/SymfonyMessengerConfig.php***

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerConfig extends SprykerSymfonyMessengerConfig
{
    /**
     * Specification:
     * - Returns transport configuration for queue transport.
     * - Each key is a queue name, each value is an array of transport options.
     * - `default` key is used for default transport configuration.
     *
     * @api
     *
     * @return array&lt;string, array&lt;string, mixed&gt;&gt;
     */
    public function getQueueTransportConfiguration(): array
    {
        return [
            &apos;default&apos; =&gt; [
                &apos;auto_setup&apos; =&gt; false,
                &apos;persistent&apos; =&gt; &apos;true&apos;,
                &apos;connect_timeout&apos; =&gt; 3,
                &apos;read_timeout&apos; =&gt; 130,
                &apos;write_timeout&apos; =&gt; 130,
                &apos;heartbeat&apos; =&gt; 0,
                &apos;rpc_timeout&apos; =&gt; 0,
            ],
        ];
    }
}

```

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

To verify that the Symfony Messenger Queue Adapter integration is working correctly:

1. Save any entity in the Back Office that should be synced to the storefront or run an import.
2. Check the RabbitMQ management interface to check if queues have messages and they are being processed.
3. Check that messages are being processed successfully and there are no errors in the logs.

{% endinfo_block %}

## Usage as a Message Consumer

Symfony Messenger is not limited to queue adapter usage. You can also use it as a message consumer for messages dispatched in your application. To use Symfony Messenger as a message consumer, configure it and enable the required plugins.
In order to use Symfony Messenger as a message consumer, you need to configure it and enable the corresponding plugins.

1. Install required transport factory.

Out of the box, Symfony Messenger module provides the AMQP as a transport option. If any other transport options is required it must be added separately. To do this, implement `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportFactoryProviderPluginInterface` that provides transport factories that can create a transport instance. A single plugin can provide multiple transport factories.

Example below will provide the `SchedulerTransportFactory` that allows to use Symfony Messenger for processing scheduled tasks in the Symfony Scheduler module, but you can provide any transport factory that you need.

```php
&lt;?php

namespace Spryker\Client\SymfonyScheduler\Plugin\SymfonyMessenger;

class SchedulerTransportFactoryProviderPlugin extends AbstractPlugin implements TransportFactoryProviderPluginInterface
{
    /**
     * {@inheritDoc}
     * - Returns SchedulerTransportFactory instance to be used by Symfony Messenger.
     *
     * @api
     *
     * @return array&lt;\Symfony\Component\Messenger\Transport\TransportFactoryInterface&gt;
     */
    public function getTransportFactories(): array
    {
        return [
            $this-&gt;getFactory()-&gt;createSchedulerTransportFactory(),//Will return an instance of SchedulerTransportFactory that is used for processing scheduled tasks in the Symfony Scheduler module.
        ];
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:


**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportFactoryProviderPluginInterface&gt;
     */
    protected function getTransportFactoryProviderPlugins(): array
    {
        return [
            new SchedulerTransportFactoryProviderPlugin(),
        ];
    }
}
```

2. Configure transports for messages

Transport factories are used to create transport instances that handle messages. Each transport is described by a name, a DSN, and an optional priority. This configuration is provided via an implementation of `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface`, which returns a map of transport name to `MessengerTransportConfigTransfer`.

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Generated\Shared\Transfer\MessengerTransportConfigTransfer;
use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarAsyncTransportConfigProviderPlugin extends AbstractPlugin implements AvailableTransportConfigProviderPluginInterface
{
    /**
     * @return array&lt;string, \Generated\Shared\Transfer\MessengerTransportConfigTransfer&gt;
     */
    public function getTransportConfigByTransportName(): array
    {
        return [
            &apos;foo_bar_async&apos; =&gt; (new MessengerTransportConfigTransfer())
                -&gt;setDsn(&apos;amqp://guest:guest@localhost:5672/eu_host&apos;)
                -&gt;setPriority(100),
        ];
    }
}
```

The `priority` defines the transport consumption order within a worker: the higher the number, the earlier the transport is polled. When omitted, priority defaults to `0`. Make sure the DSN uses a valid transport protocol (for example `amqp://` for AMQP, `redis://` for Redis, `schedule://` for the scheduler transport). A transport factory may also claim a scheme of its own: the Symfony Scheduler module registers `scheduler-amqp://` for its job queue, so that it can build the connection and serializer itself rather than taking the shared ones.

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\AvailableTransportConfigProviderPluginInterface&gt;
     */
    protected function getAvailableTransportConfigProviderPlugins(): array
    {
        return [
            new FooBarAsyncTransportConfigProviderPlugin(),
        ];
    }
}
```

3. Map messages to transports and handlers.

Message is a data object that is dispatched via Symfony Messenger and processed by the handler. Handler is a callable that contains the logic for processing the message.
Message can be any object that can be serialized and deserialized by Symfony Messenger. It can be a transfer or any other DTO. Handler must be a callable that processes the message. It can be a class that implements the `__invoke()` method or any other callable.
Yon need to map messages to handlers and transports via `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\MessageMappingProviderPluginInterface` plugin.

First, create a message and a handler that you want to map to each other.

```php
namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

class FooBarMessage
{
    protected string $data;

    public function __construct(string $data)
    {
        $this-&gt;data = $data;
    }

    public function getData(): string
    {
        return $this-&gt;data;
    }
}
```

```php
namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

class FooBarMessageHandler
{
    public function __invoke(FooBarMessage $message): void
    {
        //Handle the message
    }
}
```

And we need to map them to each other and to the transport that will handle them:

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\MessageMappingProviderPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarMappingProviderPlugin extends AbstractPlugin implements MessageMappingProviderPluginInterface
{
    public function getMessageToHandlerMap(): array
    {
        return [
            FooBarMessage::class =&gt; [
                new FooBarMessageHandler(),
            ],
        ];
    }

    public function getMessageToTransportMap(): array
    {
        return [
            FooBarMessage::class =&gt; [&apos;foo_bar_async&apos;], //DSN provided in FooBarAsyncTransportConfigProviderPlugin will be used to create a transport that will handle the message.
        ];
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

namespace Pyz\Client\SymfonyMessenger;

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    protected function getMessageMappingProviderPlugins(): array
    {
        return [
            new FooBarMessageMappingProviderPlugin(),
        ];
    }
}
```

4. Send message.

To send a message, use `SymfonyMessengerClientInterface::sendMessage()`, which the module provides. The client resolves the appropriate transport and sends the message. If the transport is synchronous, it handles the message immediately and calls the corresponding handler. Otherwise, a worker processes the message from the transport.

5. Register a consumer command.

Asynchronous messages are processed by a worker that consumes messages from the transport. To run the worker, you need to register a console command that will start it.

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

```php
&lt;?php

namespace Pyz\Zed\Console;

class ConsoleDependencyProvider extends SprykerConsoleDependencyProvider
{
    protected function getConsoleCommands(Container $container): array
    {
        return [
            new SymfonyMessengerConsumeMessagesConsole(),
        ];
    }
}
```

6. Run the worker.

```shell
console symfonymessenger:consume foo_bar_async
```

The argument is the name of the transport that you want to consume messages from. You can provide multiple transport names if you want to consume messages from different transports in one worker:

```shell
console symfonymessenger:consume foo_bar_async another_transport
```

By default, the worker will run indefinitely, but you can provide an option to stop it after a certain time in seconds:

```shell
console symfonymessenger:consume foo_bar_async --time-limit=100
```

### Consume transports in parallel

By default a single worker process polls the given transports one after another. To process a heavy workload faster, use the `--parallel` (`-p`) option to run several competing consumers. When more than one process is required, the command spawns child processes of itself instead of consuming directly:

```shell
console symfonymessenger:consume foo_bar_async --parallel=4
```

Each child process is a full worker; the output of every worker is streamed back to the parent, prefixed with `[worker-N]`, and stopping the parent (SIGTERM/SIGINT) gracefully stops all children.

{% info_block warningBox &quot;When parallel consumption is safe&quot; %}

Parallel consumption is only safe for transports where competing consumers do not process the same message twice — for example, AMQP work queues.

It gains little on scheduler (`schedule://`) transports: each schedule is guarded by a lock that the worker holding it keeps for its lifetime, so the schedules are statically split across the children rather than balanced, and a child that loses every race does nothing.

{% endinfo_block %}

### Dedicate a worker to specific transports

By default every child consumes the same transports. Use `--worker-receivers` (`-w`) to give one child a receiver list of its own, as a comma-separated set of names:

```shell
console symfonymessenger:consume foo_bar_async --worker-receivers=slow_transport --parallel=3
```

The two options are additive: each `--worker-receivers` list gets a child of its own, and `--parallel` adds that many further children consuming the receivers passed as arguments. The command above therefore runs four processes — one on `slow_transport`, three competing on `foo_bar_async`. Repeat the option to dedicate further children.

Because a dedicated list can only be honored by a child process, passing `--worker-receivers` always spawns a pool, even when it resolves to a single pair of workers.

This is how the Symfony Scheduler module separates triggering a scheduled job from running it. See [Integrate Symfony Scheduler](/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler.html#run-scheduled-jobs-asynchronously).

### Pause a transport at runtime

Sometimes a transport must be temporarily skipped by the worker without stopping the whole consumer — for example, when a scheduled job is disabled from the Back Office. Implement `\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface` to veto consumption of a transport on a per-iteration basis. Before consuming from a transport, the worker calls every guard plugin; if any of them returns `false`, that transport is skipped in the current loop iteration.

```php
&lt;?php

namespace Pyz\Zed\FooBar\Communication\Plugin\SymfonyMessenger;

use Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

class FooBarTransportConsumeGuardPlugin extends AbstractPlugin implements TransportConsumeGuardPluginInterface
{
    public function canConsumeTransport(string $transportName): bool
    {
        // Return false to skip consuming this transport in the current worker iteration.
        return true;
    }
}
```

Wire it in the dependency provider of Symfony Messenger module:

**src/Pyz/Client/SymfonyMessenger/SymfonyMessengerDependencyProvider.php**

```php
&lt;?php

class SymfonyMessengerDependencyProvider extends SprykerSymfonyMessengerDependencyProvider
{
    /**
     * @return array&lt;\Spryker\Shared\SymfonyMessengerExtension\Dependency\Plugin\TransportConsumeGuardPluginInterface&gt;
     */
    protected function getTransportConsumeGuardPlugins(): array
    {
        return [
            new FooBarTransportConsumeGuardPlugin(),
        ];
    }
}
```

{% info_block warningBox &quot;Pausing a scheduler transport freezes its schedule&quot; %}

Do not use this extension point to pause a `schedule://` transport. A paused transport never advances its schedule, so the stored position stays where it was and re-enabling the job replays every occurrence it missed in the meantime.

The Symfony Scheduler module used to ship `DisabledSchedulerJobTransportGuardPlugin` for exactly that purpose. It is deprecated and must no longer be wired — a disabled job is now skipped by the handler instead, which leaves its schedule advancing. See [Integrate Symfony Scheduler](/docs/dg/dev/integrate-and-configure/integrate-symfony-scheduler.html#monitor-and-control-scheduled-jobs-in-the-back-office).

{% endinfo_block %}

## Additional information

Because this module relies on Symfony Messenger, see the [Symfony Messenger documentation](https://symfony.com/doc/current/messenger.html) for details about configuration and usage. You can also review the module source code to understand its implementation and available features.</description>
            <pubDate>Mon, 05 Oct 2026 12:12:07 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/integrate-and-configure/integrate-symfony-messenger.html</guid>
            
            
        </item>
        
        <item>
            <title>Release notes for spryker-php 20260915</title>
            <description>This document describes the changes that have been recently released.
For additional support with this content, contact our support.
If you found a new security vulnerability, contact us at **security@spryker.com**.

---

## Release notes for spryker-php 20260915

**Improvements**:

- Introduce PHP XSL extension as optional
</description>
            <pubDate>Mon, 05 Oct 2026 08:26:50 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260915.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260915.html</guid>
            
            
        </item>
        
        <item>
            <title>Release notes for spryker-php 20260820</title>
            <description>This document describes the changes that have been recently released.
For additional support with this content, contact our support.
If you found a new security vulnerability, contact us at **security@spryker.com**.

---

## Release notes for spryker-php 20260820

**Improvements**:

- Added support for PHP 8.5.
- Updated PHP versions to their latest minor releases.
- Added support for Alpine Linux 3.24.
- Upgraded Composer to version 2.10.2.
- Upgraded Tideways to version 5.42.0.
</description>
            <pubDate>Fri, 02 Oct 2026 15:38:27 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260820.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260820.html</guid>
            
            
        </item>
        
        <item>
            <title>Release notes for spryker-php 20260908</title>
            <description>This document describes the changes that have been recently released.
For additional support with this content, contact our support.
If you found a new security vulnerability, contact us at **security@spryker.com**.

---

## Release notes for spryker-php 20260908

**Improvements**:

- Update Debian version.
- Update PHP to the latest minors.
- Update NewRelic to the latest version.
</description>
            <pubDate>Fri, 02 Oct 2026 15:31:31 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260908.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/releases/image-releases/spryker-php/release-notes-spryker-php-20260908.html</guid>
            
            
        </item>
        
        <item>
            <title>Common pitfalls in OMS design</title>
            <description>&lt;p&gt;Implementing OMS processes can be challenging when they are complex, or requirements are not trivial. This can lead to hidden issues which are hard to debug. An example of such issues is race conditions.&lt;/p&gt;
&lt;p&gt;In some cases, OMS works incorrectly. In most cases, a &lt;em&gt;correct&lt;/em&gt; flow can be run successfully, but the first run of a wrong path might reveal a problem. In other cases, there might be known limitations that can lead to incorrect transitions. There can also be cases that are valid but should be rewritten into a better readable process. If you discover more edge cases,  send those to our &lt;a href=&quot;https://support.spryker.com&quot;&gt;support team&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This document describes the most common issues with OMS design and how you can fix them.&lt;/p&gt;
&lt;h2 id=&quot;more-than-one-onenter-event-from-one-state&quot;&gt;More than one onEnter event from one state&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: If there is more than one onEnter transition event from state A, only one is executed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/oms-processing-1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason&lt;/strong&gt;: This behavior is not supported because there must always be only one state after an event execution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: If you have different commands, you can chain those:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/oms-processing-2.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you have the same commands, then one of the commands could get a condition:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/oms-processing-3.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;defining-states-with-names&quot;&gt;Defining states with names&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; States with the same names are declared in several processes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason:&lt;/strong&gt; When a process or a sub-process is read, transitions are assigned to the &lt;em&gt;from&lt;/em&gt;-state in the current process or sub-process. In case of several declarations, these will be different &lt;em&gt;from&lt;/em&gt;-states.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Define states with unique names only. We recommend defining the state in the process which introduces it. You can define transition in other states.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Declare the state in the process where it has outgoing transitions.&lt;/p&gt;
&lt;h2 id=&quot;duplicate-events&quot;&gt;Duplicate events&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; Events with the same names are declared in multiple processes.&lt;/p&gt;
&lt;p&gt;In the OMS drawing, you will see the last &lt;em&gt;read&lt;/em&gt; event definition, but during the execution, any might be defined.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solutions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rename one of the events.&lt;/li&gt;
&lt;li&gt;Keep only one event. We recommend having reusable events in the main process rather than using those from some sub-processes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;states-with-only-outgoing-transitions&quot;&gt;States with only outgoing transitions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: There are many states with only outgoing transitions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/oms-processing-4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason&lt;/strong&gt;: Function &lt;code&gt;OmsConfig:getInitialStatus&lt;/code&gt; has only one return value, so it’s impossible to start from another “initial” state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: In most cases, this is a mistake, and the transition between some states is missing. Adding transition makes the process correct. For example, adding transition &lt;code&gt;payment done&lt;/code&gt; → &lt;code&gt;shipped&lt;/code&gt; with the &lt;code&gt;ship&lt;/code&gt; event brings the whole process to a correct state.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-2-fixed.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&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;You can change order items’ states through a manual call, and this way, use the states without inbound transitions. On the one hand, this helps not to overwhelm the process with 10+ transitions to a cancellation process. On the other hand, though, this makes the process definition incomplete and, thus, this approach is not recommended.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;more-than-one-main-process&quot;&gt;More than one main process&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; Having more than one main process can lead to incorrect process rendering and execution:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-7.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When placing an order, this issue entails an error like this one:&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;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unknown&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/data/vendor/spryker/oms/src/Spryker/Zed/Oms/
Business/Process/Process.php::198&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;exception&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;[object] (Exception(code: 0):
Unknown state: new at /data/vendor/spryker/oms/src/Spryker/Zed/Oms/Business/
Process/Process.php:198)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Removing duplicate flag  &lt;code&gt;main&lt;/code&gt; fixes the process rendering and processing:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/oms-processing-5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;more-than-one-transition-with-the-same-events-and-without-condition&quot;&gt;More than one transition with the same events and without condition&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: It’s impossible to guess which transition is expected, so the first one read is executed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution 1:&lt;/strong&gt;  Add a condition to one of the transitions:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-5-fixed.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution 2:&lt;/strong&gt;  Change event on one transition:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-5-fixed-2.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;conditional-timeout-and-manual-transitions-mixture-from-the-same-state&quot;&gt;Conditional, timeout, and manual transitions mixture from the same state&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Creating a condition and a timeout, or a manual event and a condition, or a manual event and a timeout from the same state leads to errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason&lt;/strong&gt;: Condition check and timeout execution happen in different console commands, and the order of the execution is not defined by the OMS but by a scheduler.&lt;/p&gt;
&lt;p&gt;Execution of the manual event could also happen during the console command execution, and the resulting state of the order items, in this case, is unpredictable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rewrite the process and check the condition after the timeout.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;TimeoutProcessor&lt;/code&gt;. It significantly decreases the probability of the same-time execution.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;use-asynchronous-break-without-command-and-condition&quot;&gt;Use asynchronous break without command and condition&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; Adding timeout pauses the execution process and unblocks external systems.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-9.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reasons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Timeout triggers the DB update, namely—the creation of a timeout entry for each order item in the &lt;code&gt;spy_oms_event_timeout&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;Timeout check requires a DB query to find the affected entries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: We recommend using an event (&lt;code&gt;pause&lt;/code&gt;) without command and without condition. The event will not interact with the DB and will also be executed with the next run of the &lt;code&gt;oms:check-condition&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/image-20210924-103858.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;not-used-events-or-states&quot;&gt;Not used events or states&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; Process contains a declaration of a state which is never used:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-15.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The unused state could have a missing transition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Delete a state, or add a missing transition:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-15-solution.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;incorrect-event-definition&quot;&gt;Incorrect event definition&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; Mixing  &lt;code&gt;onEnter=&amp;quot;true&amp;quot;&lt;/code&gt; and &lt;code&gt;manual=&amp;quot;true&amp;quot;&lt;/code&gt; in the same event.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-10.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Event does not appear as manual unless the previous command execution fails with an exception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Create separate transitions: one with the &lt;code&gt;onEnter&lt;/code&gt; command, the other with the &lt;code&gt;manual&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/scos/dev/back-end-development/data-manipulation/datapayload-conversion/state-machine/common-pitfalls-in-oms-design/oms-issue-10-fixed.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&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;Tip&lt;/div&gt;
&lt;p&gt;Keeping both &lt;code&gt;onEnter&lt;/code&gt; and &lt;code&gt;manual&lt;/code&gt; commands can only be used for backup for the failed automated execution of the &lt;code&gt;onEnter&lt;/code&gt; command with a manual event.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;calling-oms-processing-functions-within-a-custom-db-transaction&quot;&gt;Calling OMS processing functions within a custom DB transaction&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; You want to enclose complex processing, including OMS processing functions, inside a transaction.&lt;/p&gt;
&lt;p&gt;OMS processing functions, like triggerEvent*, checkConditions and checkTimeouts, use lock on the order item level to prevent processing of the same item more than once at the same time. The lock information is stored as an entry in the &lt;code&gt;spy_oms_state_machine_lock&lt;/code&gt; table.
Running this code inside a DB transaction make the lock entries inaccessible. This may lead to an undetermined resulting state of the item or even to a DB deadlock in rare cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Avoid OMS processing function calls inside DB transactions.&lt;/p&gt;
&lt;h2 id=&quot;oms-configuration-changes-not-applied&quot;&gt;OMS configuration changes not applied&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; You updated OMS configuration in an XML file, but the changes are not applied.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; OMS processes are cached by default. If &lt;code&gt;OmsConstants::ENABLE_PROCESS_CACHE&lt;/code&gt; is not set to false in configuration files, you need to regenerate the cache every time you update OMS configuration.&lt;/p&gt;
&lt;p&gt;Regenerate cache:&lt;/p&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;vendor/bin/console oms:process-cache:warm-up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;slow-checkout-endpoint&quot;&gt;Slow checkout endpoint&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; During the checkout process, order items are created in the &lt;code&gt;new&lt;/code&gt; status by default and immediately become part of the OMS workflow. Any &lt;code&gt;onEnter&lt;/code&gt; event with command from the state &lt;code&gt;new&lt;/code&gt; is executed within the same PHP process as the checkout. This can significantly increase processing time of checkout requests.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/coupled_new_state_to_command.png&quot; alt=&quot;coupled_new_state_to_command&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Postpone all subsequent transitions from the &lt;code&gt;new&lt;/code&gt; state. Configure transitions to be executed in the background by Jenkins triggering &lt;code&gt;console oms:check-condition&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spryker.s3.eu-central-1.amazonaws.com/docs/pbc/all/order-management-system/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.md/decoupled_new_state_from_command.png&quot; alt=&quot;decoupled_new_state_from_command&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;language-xml 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;nt&quot;&gt;&amp;lt;transitions&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;transition&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;happy=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;source&amp;gt;&lt;/span&gt;new&lt;span class=&quot;nt&quot;&gt;&amp;lt;/source&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;target&amp;gt;&lt;/span&gt;ready for confirmation&lt;span class=&quot;nt&quot;&gt;&amp;lt;/target&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/transition&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;transition&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;happy=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;source&amp;gt;&lt;/span&gt;ready for confirmation&lt;span class=&quot;nt&quot;&gt;&amp;lt;/source&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;target&amp;gt;&lt;/span&gt;confirmation sent&lt;span class=&quot;nt&quot;&gt;&amp;lt;/target&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;event&amp;gt;&lt;/span&gt;confirmation&lt;span class=&quot;nt&quot;&gt;&amp;lt;/event&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/transition&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/transitions&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;events&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;event&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;confirmation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onEnter=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Oms/SendOrderConfirmation&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/events&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2 id=&quot;deferred-processing-of-new-order-items&quot;&gt;Deferred processing of new order items&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; By default, the checkout request runs the first state machine iteration for new order items, including reservations, &lt;code&gt;onEnter&lt;/code&gt; events, and timeouts of the initial state. This makes order placement depend on state machine logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Enable deferred processing of new order items. With this flag, the checkout places the order as soon as possible and only initializes the state machine for its items. State machine processing is permanently moved to the background, where &lt;code&gt;oms:check-timeout&lt;/code&gt; executes it.&lt;/p&gt;
&lt;p&gt;To enable deferred processing, update &lt;code&gt;spryker/oms&lt;/code&gt; to version 11.55.0 or higher and enable the flag:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/Pyz/Zed/Oms/OmsConfig.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\Oms&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\Oms\OmsConfig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SprykerOmsConfig&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;OmsConfig&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SprykerOmsConfig&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;isDeferredNewOrderItemProcessingEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&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;kc&quot;&gt;true&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;section class=&apos;info-block info-block--warning&apos;&gt;&lt;i class=&apos;info-block__icon icon-warning&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;&lt;div class=&quot;info-block__title&quot;&gt;Warning&lt;/div&gt;
&lt;p&gt;Make sure the &lt;code&gt;oms:check-timeout&lt;/code&gt; command is scheduled. New order items stay in their initial state, and their stock is not reserved until the command processes them.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</description>
            <pubDate>Fri, 02 Oct 2026 12:31:26 +0000</pubDate>
            <link>https://docs.spryker.com/docs/pbc/all/order-management-system/latest/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/pbc/all/order-management-system/latest/base-shop/datapayload-conversion/state-machine/common-pitfalls-in-oms-design.html</guid>
            
            
        </item>
        
        <item>
            <title>Add and configure cronjobs</title>
            <description>&lt;p&gt;This document shows how to add and configure cronjobs in Jenkins.&lt;/p&gt;
&lt;p&gt;We use &lt;a href=&quot;https://jenkins-ci.org/&quot;&gt;Jenkins&lt;/a&gt; for cronjob scheduling. Compared to Crontab, there are several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jobs are queued and can be manually executed.&lt;/li&gt;
&lt;li&gt;Job definitions are under version control and can be changed by any developer.&lt;/li&gt;
&lt;li&gt;Console output is available for debugging.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;add-a-new-job-and-run-it&quot;&gt;Add a new job and run it&lt;/h2&gt;
&lt;p&gt;Jobs are defined in &lt;code&gt;config/Zed/cronjobs/jenkins.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This file contains an array defining the jobs.&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;c1&quot;&gt;// Send emails every 10 minutes&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$jobs&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;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;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;send-mails&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;command&apos;&lt;/span&gt;               &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;$PHP_BIN vendor/bin/console mail:send-mail&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;schedule&apos;&lt;/span&gt;              &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;*/10 * * * *&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;enable&apos;&lt;/span&gt;                &lt;span class=&quot;o&quot;&gt;=&amp;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;s1&quot;&gt;&apos;stores&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&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;FR&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;To import this configuration to Jenkins, run &lt;code&gt;vendor/bin/console scheduler:setup&lt;/code&gt;. In a production environment, this is part of the default Normal deployment process.&lt;/p&gt;
&lt;section class=&apos;info-block info-block--error&apos;&gt;&lt;i class=&apos;info-block__icon icon-error&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;
&lt;p&gt;When you remove jobs from &lt;code&gt;config/Zed/cronjobs/jenkins.php&lt;/code&gt;, the application cannot detect the removal, so the removed jobs stay in Jenkins in their last state.&lt;/p&gt;
&lt;p&gt;To stop them, do so manually or through a support request.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h2 id=&quot;cronjob-configuration&quot;&gt;Cronjob configuration&lt;/h2&gt;
&lt;p&gt;For each job you can define several configurations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;TYPE&lt;/th&gt;
&lt;th&gt;PURPOSE&lt;/th&gt;
&lt;th&gt;✓&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Name of the job.&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;command&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;The &lt;a href=&quot;/docs/dg/dev/backend-development/console-commands/implement-console-commands.html&quot;&gt;console command&lt;/a&gt; that is executed.&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schedule&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Expression that defines the job schedule (how often the job runs). The schedule string is compatible with the cronjob schedule definition—for example, &lt;code&gt;0 * * * *&lt;/code&gt; means run once each hour at minute 00. If the environment is in development, return an empty string—on the development environment, cronjobs run manually only.&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;enable&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;Enable or disable jobs.&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stores&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;An array of stores where the job is executed.&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;section class=&apos;info-block info-block--error&apos;&gt;&lt;i class=&apos;info-block__icon icon-error&apos;&gt;&lt;/i&gt;&lt;div class=&apos;info-block__content&apos;&gt;
&lt;p&gt;When you do not use Jenkins for job scheduling, there is no locking between concurrently running commands.&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
&lt;h3 id=&quot;troubleshoot-jenkins-errors&quot;&gt;Troubleshoot Jenkins errors&lt;/h3&gt;
&lt;p&gt;If during console command execution any request to Jenkins API fails, the command exits with a non-zero status and reports the affected job together with the Jenkins HTTP error and response.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Scheduler Status: ERROR
- DE_consume-queue
Scheduler Error: Job &quot;DE_consume-queue&quot;: Jenkins request POST ... failed with HTTP 400 Bad Request. Jenkins error: A job already exists with the name &quot;DE_consume-queue&quot;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
            <pubDate>Fri, 02 Oct 2026 10:46:54 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/backend-development/cronjobs/add-and-configure-cronjobs.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/backend-development/cronjobs/add-and-configure-cronjobs.html</guid>
            
            
        </item>
        
        <item>
            <title>Uninstall the Marketplace from B2B Demo Marketplace</title>
            <description>This document describes how to uninstall marketplace features from
the [B2B Demo Marketplace](https://github.com/spryker-shop/b2b-demo-marketplace) to transform it into a standard B2B
Demo Shop.

{% info_block infoBox &quot;Info&quot; %}

These instructions are one-time use only, you don&apos;t need to run these scripts after the project was started.

{% endinfo_block %}

## Overview

The B2B Demo Marketplace comes with all marketplace-specific features pre-installed, including the Merchant Portal,
marketplace product management, and merchant-related functionality. If you want to use the B2B Demo Marketplace as a
starting point for a standard B2B project without marketplace features, you can use the provided uninstallation scripts
to remove all marketplace-specific features.

The uninstallation process consists of two main scripts:

| Script                              | Purpose                                                                                                                                    |
|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `uninstall-marketplace-modules.sh`  | Removes backend marketplace features, including Composer packages, PHP dependency providers, configuration files, and data import entities |
| `uninstall-frontend-marketplace.sh` | Removes frontend marketplace files, including Merchant Portal configuration, Angular dependencies, and frontend build configurations       |

## Prerequisites

Before you begin the uninstallation process, make sure you have the following:

- A latest working version of B2B Demo Marketplace installation based:
  on [https://github.com/spryker-shop/b2b-demo-marketplace](https://github.com/spryker-shop/b2b-demo-marketplace)
- Docker SDK installed and configured
- Python 3 installed on your system
- Node.js and npm installed
- Sufficient disk space for rebuilding the application

## Uninstall marketplace features

### 1. Locate the uninstallation scripts

The uninstallation scripts are included in the B2B Demo Marketplace repository. After cloning the repository, you can
find them in the root directory:

- [uninstall-marketplace-modules.sh](https://github.com/spryker-shop/b2b-demo-marketplace/blob/master/uninstall-marketplace-modules.sh) -
  Backend uninstallation script
- [uninstall-marketplace-config.json](https://github.com/spryker-shop/b2b-demo-marketplace/blob/master/uninstall-marketplace-config.json) -
  Configuration file for the backend script
- [uninstall-frontend-marketplace.sh](https://github.com/spryker-shop/b2b-demo-marketplace/blob/master/uninstall-frontend-marketplace.sh) -
  Frontend uninstallation script

### 2. Make the scripts executable

Run the following commands to make the scripts executable:

```bash
chmod +x uninstall-marketplace-modules.sh
chmod +x uninstall-frontend-marketplace.sh
```

### 3. Run the backend uninstallation script

Execute the backend uninstallation script to remove all marketplace-specific backend features:

```bash
./uninstall-marketplace-modules.sh
```

This script performs the following operations:

1. **Removes Marketplace Features**: Uninstalls all `spryker-feature/marketplace-*` Composer packages
2. **Removes Marketplace Core Modules**: Uninstalls individual `spryker/*` marketplace modules
3. **Cleans PHP Dependency Providers**: Removes marketplace-specific plugins, event subscribers, and configurations from
   dependency provider files
4. **Updates Configuration Files**: Removes marketplace-related settings from configuration files
5. **Removes Marketplace Directories**: Deletes marketplace-specific module directories from `src/Pyz/`
6. **Updates Data Import Configuration**: Removes marketplace data import entities from YAML import files
7. **Updates Docker Configuration**: Removes Merchant Portal commands from Docker installation files
8. **Updates Deploy Files**: Removes Merchant Portal application definitions from deploy files
9. **Processes XML and Test Files**: Updates OMS configurations and test files as needed

The script displays progress messages for each step. Review the output to make sure all operations complete
successfully.

### 4. Run the frontend uninstallation script

After the backend uninstallation completes, execute the frontend uninstallation script:

```bash
./uninstall-frontend-marketplace.sh
```

This script performs the following operations:

1. **Removes Frontend Configuration Files**: Deletes `tsconfig.mp.json`, `.stylelintrc.mp.js`, and `angular.json`
2. **Removes Frontend Directories**: Deletes the `frontend/merchant-portal` directory
3. **Cleans ESLint Configuration**: Removes Merchant Portal configuration blocks from `eslint.config.mjs`
4. **Cleans package.json**: Removes `mp:*` scripts and Angular-related dependencies
5. **Updates npm Dependencies**: Runs `npm install` to update the dependency tree
6. **Installs Required Packages**: Installs `@babel/core` and `@babel/runtime` packages
7. **Cleans npm Cache**: Removes the `node_modules/.cache` directory

### 5. Rebuild the application

After both scripts complete, rebuild your application to apply the changes:

```bash
docker/sdk clean &amp;&amp; docker/sdk boot deploy.dev.yml &amp;&amp; docker/sdk up
```

{% info_block infoBox &quot;Info&quot; %}

Use the deploy file appropriate for your environment. For production-like environments, use `deploy.yml` instead of
`deploy.dev.yml`.

{% endinfo_block %}

### 6. Verify the uninstallation

To verify that the marketplace features have been successfully removed:

1. Check that the Merchant Portal is no longer accessible
2. Verify that marketplace-specific database tables are not being used
3. Check that no marketplace-related errors appear in the application logs
4. Test the B2B storefront functionality to ensure it works correctly

### 7. Remove the uninstallation scripts

After successfully uninstalling the marketplace features, remove the uninstallation scripts from your project:

```bash
rm uninstall-marketplace-modules.sh
rm uninstall-marketplace-config.json
rm uninstall-frontend-marketplace.sh
```

{% info_block warningBox &quot;Security recommendation&quot; %}

Always remove the uninstallation scripts after completing the uninstallation process. These scripts should never be deployed to production environments, as they can make destructive changes to your codebase and configuration.

{% endinfo_block %}

## What gets removed

The uninstallation scripts remove the following features:

&lt;details&gt;
&lt;summary&gt;Marketplace features removed&lt;/summary&gt;

- `spryker-feature/marketplace-agent-assist`
- `spryker-feature/marketplace-cart`
- `spryker-feature/marketplace-comments`
- `spryker-feature/marketplace-merchant-commission`
- `spryker-feature/marketplace-merchant-contract-requests`
- `spryker-feature/marketplace-merchant-contracts`
- `spryker-feature/marketplace-merchant-custom-prices`
- `spryker-feature/marketplace-merchant-order-threshold`
- `spryker-feature/marketplace-merchant-portal-product-management`
- `spryker-feature/marketplace-merchant-portal-product-offer-management`
- `spryker-feature/marketplace-merchant-portal-product-offer-service-points`
- `spryker-feature/marketplace-merchant-portal-product-offer-shipment`
- `spryker-feature/marketplace-merchantportal-core`
- `spryker-feature/marketplace-order-management`
- `spryker-feature/marketplace-packaging-units`
- `spryker-feature/marketplace-product-approval-process`
- `spryker-feature/marketplace-product-options`
- `spryker-feature/marketplace-promotions-discounts`
- `spryker-feature/marketplace-return-management`
- `spryker-feature/marketplace-shopping-lists`
- `spryker-feature/merchant-category`
- `spryker-feature/merchant-opening-hours`
- `spryker-feature/merchant-portal-data-import`
- `spryker-feature/product-approval-process`

&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;Marketplace core modules removed&lt;/summary&gt;

- `spryker/agent-dashboard-merchant-portal-gui`
- `spryker/agent-security-blocker-merchant-portal-gui`
- `spryker/agent-security-merchant-portal-gui`
- `spryker/availability-merchant-portal-gui`
- `spryker/cart-note-merchant-portal-gui`
- `spryker/category-merchant-commission-connector`
- `spryker/dashboard-merchant-portal-gui`
- `spryker/dummy-marketplace-payment`
- `spryker/merchant-app-merchant-portal-gui`
- `spryker/merchant-categories-rest-api`
- `spryker/merchant-discount-connector`
- `spryker/merchant-opening-hours-rest-api`
- `spryker/merchant-product-offer-shopping-lists-rest-api`
- `spryker/merchant-product-shopping-lists-rest-api`
- `spryker/merchant-products-rest-api`
- `spryker/merchant-profile-merchant-portal-gui`
- `spryker/merchant-sales-returns-rest-api`
- `spryker/merchant-shipments-rest-api`
- `spryker/merchants-rest-api`
- `spryker/multi-factor-auth-merchant-portal`
- `spryker/price-product-merchant-commission-connector`
- `spryker/product-merchant-commission-connector`
- `spryker/product-option-merchant-portal-gui`
- `spryker/sales-merchant-portal-gui`
- `spryker/security-blocker-merchant-portal-gui`
- `spryker/tax-merchant-portal-gui`

&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;Directories removed&lt;/summary&gt;

- `src/Pyz/Zed/MerchantPortalApplication`
- `src/Pyz/Zed/MerchantSalesOrder`
- `src/Pyz/Zed/MerchantOms`
- `src/Pyz/Zed/Merchant`
- `src/Pyz/Zed/MerchantUser`
- Other marketplace-specific module directories
- `public/MerchantPortal`

&lt;/details&gt;

## Troubleshooting

### Script fails with &quot;Configuration file not found&quot; error

Make sure the `uninstall-marketplace-config.json` file is placed in the same directory as the
`uninstall-marketplace-modules.sh` script.

### Composer removal fails

If Composer fails to remove packages, try running the removal command manually with additional flags:

```bash
composer remove --ignore-platform-req=ext-grpc --ignore-platform-req=ext-redis PACKAGE_NAME
```

### Application fails to build after uninstallation

1. Clear all caches:

```bash
docker/sdk cli console cache:empty-all
```

2. Regenerate transfer objects:

```bash
docker/sdk cli console transfer:generate
```

3. Run a full rebuild:

```bash
docker/sdk prune &amp;&amp; docker/sdk boot deploy.dev.yml &amp;&amp; docker/sdk up
```

### Database errors after uninstallation

If you encounter database-related errors, you may need to drop and recreate your database:

```bash
docker/sdk cli console propel:install
```

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

This command drops all existing data. Use it only in development environments or if you have a proper backup.

{% endinfo_block %}

## Customizing the uninstallation

The uninstallation behavior is controlled by the `uninstall-marketplace-config.json` configuration file. You can modify
this file to:

- Add or remove packages from the uninstallation list
- Adjust which files and directories are processed
- Customize the cleanup operations for specific files

For detailed information about the configuration file structure and available operations, see
the [UNINSTALL_MAINTENANCE_GUIDE.md](https://github.com/spryker-shop/b2b-demo-marketplace/blob/master/UNINSTALL_MAINTENANCE_GUIDE.md)
file.
</description>
            <pubDate>Thu, 01 Oct 2026 12:15:40 +0000</pubDate>
            <link>https://docs.spryker.com/docs/about/all/uninstall-marketplace-from-b2b-demo-marketplace.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/about/all/uninstall-marketplace-from-b2b-demo-marketplace.html</guid>
            
            
        </item>
        
        <item>
            <title>Upgrade to frontend builder v2 for Yves</title>
            <description>This document provides instructions for upgrading from the legacy Yves frontend builder, located in the project&apos;s `frontend/` directory, to frontend builder v2, which ships inside the ShopUi module.

For an overview of the new builder and its features, see [Frontend builder for Yves v2](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2.html).

*Estimated migration time: 1h*

## Prerequisites

Frontend builder v2 is executed by Node.js directly as TypeScript, so it requires:

- Node.js 24.15.0 or later
- npm 10 or later
- `spryker-shop/shop-ui` version 2.1.0 or later

## 1) Update composer packages

ShopUi 2.0.0 is a new major version, and most Yves modules constrain ShopUi to `^1.x`, so they must be updated together with it. Each of them has a release that allows ShopUi 2—see [Storefront modules released with builder v2](#storefront-modules-released-with-builder-v2).

1. Several feature packages constrain ShopUi and moved to semantic versioning. If your project requires them, update their constraints in `composer.json`:

```json
&quot;spryker-feature/ai-commerce&quot;: &quot;^0.7.8&quot;,
&quot;spryker-feature/buy-box&quot;: &quot;^1.4.0&quot;,
&quot;spryker-feature/purchasing-control&quot;: &quot;^1.2.0&quot;,
&quot;spryker-feature/self-service-portal&quot;: &quot;^20.12.0&quot;,
```

{% info_block warningBox &quot;spryker-feature/spryker-core&quot; %}

All `spryker-feature/spryker-core` releases up to and including 202606.0 constrain ShopUi to `^1.109.0`. ShopUi 2 support ships with the next `spryker-feature/spryker-core` release; this migration requires updating the package to that release.

{% endinfo_block %}

2. Require ShopUi 2 and update it together with all modules that depend on it:

```bash
composer require spryker-shop/shop-ui:&quot;^2.1.0&quot; --no-update
composer update spryker-shop/shop-ui &quot;spryker-shop/*&quot; &quot;spryker-feature/*&quot; --with-dependencies
```

Running `composer require spryker-shop/shop-ui:&quot;^2.0.0&quot;` on its own fails: the locked module versions constrain ShopUi to `^1.x`, and composer doesn&apos;t update packages that aren&apos;t listed in the update command. The wildcard arguments put every Spryker storefront and feature package into the update set, so composer can pick the releases that allow ShopUi 2.

Packages that moved to semantic versioning are reported as downgrades—for example, `spryker-feature/purchasing-control (202606.0 =&gt; 1.2.0)`. This is expected: the semver releases continue the date-based release lines.

{% info_block infoBox &quot;The composer update and the builder switch can be shipped separately&quot; %}

ShopUi 2 keeps the `Theme/default` structure the legacy builder relies on (`styles/basic.scss`, `util.scss`, `app/`, `components/`), so after this step the project still builds with the legacy builder. You can ship the package update first and switch to builder v2 in a follow-up—the intermediate state builds and runs. Until the switch, expect a large number of Sass deprecation warnings coming from the updated module styles; builder v2 resolves them together with the new entry-point contract.

{% endinfo_block %}

## 2) Update Node.js and npm versions

1. In all `deploy.*.yml` files used by the frontend, set the Node.js and npm versions:

```yaml
image:
    ...
    node:
        version: 24
        npm: 10
```

2. If the project has an `.nvmrc` file, update it:

```text
24
```

## 3) Update `package.json`

1. Update the engines:

```json
&quot;engines&quot;: {
    &quot;node&quot;: &quot;&gt;=24.14.0&quot;,
    &quot;npm&quot;: &quot;&gt;=10.0.0&quot;
}
```

2. Declare the ShopUi module as an npm workspace, so that npm installs the builder&apos;s dependencies:

```json
&quot;workspaces&quot;: [
    &quot;vendor/spryker-shop/shop-ui&quot;
]
```

For what this does and how it changes the commands, see [npm workspaces for the frontend builders](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html).

3. Replace the `yves:*` scripts so they delegate to that workspace:

```json
&quot;scripts&quot;: {
    &quot;yves&quot;: &quot;npm run build -w shop-ui --&quot;,
    &quot;yves:watch&quot;: &quot;npm run build:watch -w shop-ui --&quot;,
    &quot;yves:production&quot;: &quot;npm run build:production -w shop-ui --&quot;,
    &quot;yves:stylelint&quot;: &quot;npm run stylelint -w shop-ui --&quot;,
    &quot;yves:lint&quot;: &quot;npm run lint -w shop-ui --&quot;
}
```

From ShopUi 2.3.0, also add the configuration generation to `postinstall`, so that `npm install` regenerates `tsconfig.yves.json` — see [step 6](#6-verify-tsconfigyvesjson):

```json
&quot;postinstall&quot;: &quot;npm run update:config -w shop-ui&quot;
```

4. Remove the scripts that no longer exist:

```json
&quot;yves:help&quot;: &quot;node ./frontend/libs/command-line-parser --help&quot;,
&quot;yves:stylelint:fix&quot;: &quot;node ./frontend/libs/stylelint.mjs --fix&quot;,
&quot;yves:lint:fix&quot;: &quot;...&quot;
```

The parameter overview previously provided by `yves:help` is now available via `npm run yves -- --help`.

5. Remove the Yves build dependencies that ShopUi declares now, and keep the ones it declares as peer dependencies. `vendor/spryker-shop/shop-ui/package.json` is the source of truth for both — see [Where the npm dependencies come from](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html#where-the-npm-dependencies-come-from).

From ShopUi 2.3.0, the module declares the whole toolchain itself and has no peer dependencies, so the project declares nothing for the Yves build. Everything below is installed automatically through the `shop-ui` workspace and goes from your `package.json`:

- Toolchain: `@babel/core`, `@babel/plugin-transform-class-properties`, `@babel/plugin-transform-runtime`, `@babel/preset-env`, `@babel/preset-typescript`, `@babel/runtime`, `@colordx/core`, `@jest/globals`, `@jsdevtools/file-path-filter`, `@types/node`, `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser`, `autoprefixer`, `babel-loader`, `chokidar`, `commander`, `compression-webpack-plugin`, `copy-webpack-plugin`, `css-loader`, `css-minimizer-webpack-plugin`, `eslint`, `fast-glob`, `jest`, `mini-css-extract-plugin`, `postcss`, `postcss-loader`, `postcss-selector-parser`, `sass-embedded`, `sass-loader`, `style-dictionary`, `stylelint`, `stylelint-config-standard-scss`, `ts-jest`, `typescript`, `webpack`, `webpack-merge`.
- Runtime libraries of the ShopUi components: `autonumeric`, `lodash-es`, `password-validator`.

On ShopUi 2.1 and 2.2, keep the packages the module lists in `peerDependencies`. Either way, `vendor/spryker-shop/shop-ui/package.json` is the source of truth — see [What you can remove from your package.json](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html#what-you-can-remove-from-your-packagejson).

Three packages go regardless of that split, because the new builder does not use them at all:

```json
&quot;sass&quot;: &quot;x.x.x&quot;,
&quot;sass-resources-loader&quot;: &quot;x.x.x&quot;,
&quot;terser-webpack-plugin&quot;: &quot;x.x.x&quot;
```

The shared Sass context (settings, helpers, and cross-component mixins) is injected by the builder itself, so `sass-resources-loader` has no consumer left. Check for project-owned webpack configurations that still reference it before removing it.

## 4) Remove the legacy builder from the `frontend/` directory

Delete the legacy builder code:

```bash
rm -rf frontend/build.js frontend/configs frontend/libs frontend/settings.js
```

Keep the following directories—they are project content, not builder code:

- `frontend/assets/`—project asset sources (images, fonts, icons)
- `frontend/static/`—static public files
- `frontend/merchant-portal/`—Merchant Portal build tooling (unrelated to the Yves builder)

## 5) Migrate custom builder settings

If your project never modified `frontend/settings.js` or `frontend/configs/*`, skip this step—the new builder works with zero configuration.

If the project registered custom namespaces or paths in `frontend/settings.js`, recreate them in the new override file `frontend/yves.settings.mts`:

```ts
// frontend/yves.settings.mts
import { defineConfig } from &apos;../vendor/spryker-shop/shop-ui/src/SprykerShop/Yves/ShopUi/FrontendBuilder/settings.mts&apos;;

export default defineConfig({
    paths: {
        sources: {
            newNamespace: &apos;./PATH_TO_YOUR_FOLDER&apos;,
        },
    },
});
```

Unlike v1, a single `sources` entry is enough—there are no separate `dirs` arrays to keep in sync. For the full list of supported overrides (`paths.sources`, `paths.iconSprite`, `buildHooks`), see [Frontend builder for Yves v2](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2.html#project-level-builder-settings).

{% info_block warningBox &quot;Webpack config customizations&quot; %}

Direct webpack config customizations from `frontend/configs/development.js` or `frontend/configs/production.js` have no direct equivalent: the webpack configuration is owned by the builder. Most common customizations are covered by `defineConfig()` overrides and build hooks; review any remaining ones case by case.

{% endinfo_block %}

## 6) Verify `tsconfig.yves.json`

The builder reads component path aliases from `tsconfig.yves.json` and requires the `ShopUi` alias to locate the core shared stylesheet.

Up to ShopUi 2.2, the file lives at the project root and is maintained by hand. Make sure the alias is present:

```json
&quot;paths&quot;: {
    &quot;ShopUi/*&quot;: [&quot;./vendor/spryker-shop/shop-ui/src/SprykerShop/Yves/ShopUi/Theme/default/*&quot;]
}
```

From ShopUi 2.3.0, the file is generated. Run the generation once, and `postinstall` keeps it current afterwards:

```bash
npm run update:config -w shop-ui
```

It writes `tsconfig.yves.json` inside the builder directory with the aliases of every installed module. If the project keeps a `tsconfig.yves.json` in its root, the generation reconciles the aliases and `include` globs in that file instead and writes no second copy, so a root copy can stay. For the ownership split, see [Generated TypeScript configuration](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2.html#generated-typescript-configuration).

## 7) Install and build

1. Update `package-lock.json` and install dependencies:

```bash
docker/sdk cli npm install
```

{% info_block warningBox &quot;Don&apos;t delete package-lock.json&quot; %}

Let `npm install` update the lock file in place. Deleting it and resolving the tree from scratch drops any `overrides` that target a dependency inside a workspace—`vendor/spryker/*` and `vendor/spryker/*/assets/Zed` are npm workspaces in a Spryker project—because npm 10 doesn&apos;t apply overrides to those subtrees. Security pins added that way disappear silently and the vulnerable transitive versions come back.

{% endinfo_block %}

2. Build the Yves frontend:

```bash
docker/sdk cli npm run yves
```

3. Verify the build:

- The build output starts with a `Source layout: project (modules in vendor/)` line.
- Assets are generated into `public/Yves/assets/&lt;namespace&gt;/&lt;theme&gt;/` as before.
- The storefront renders with the expected styles.

## Minimum changes to legacy project styles

Migrating the project styles to the Sass module system is **not** part of this upgrade. Project component files keep working in the legacy form—the builder injects the shared context (settings, helpers, and cross-component mixins) into every component file at compile time, so they need no `@use` rules of their own. Files that emit CSS at the top level compile with a legacy style rescue warning and stay in the bundles.

One legacy pattern fails to compile and must be adjusted: a shared helper mixin that reads a variable defined in the consuming component file.

```scss
// styles/helpers/_product-shared.scss
@mixin product-shared() {
    width: rem($image-size);
}

// components/molecules/my-product/my-product.scss
$image-size: 76;

@include product-shared();
```

The v1 builder prepended all styles into one global scope, so the mixin picked such a variable up dynamically at include time. Builder v2 loads the shared stylesheet as a Sass module, and a module mixin resolves variables lexically—in the stylesheet that defines the mixin—so the build fails with `Undefined variable` pointing at the mixin body.

Pass the value as a mixin parameter with a default instead:

```scss
// styles/helpers/_product-shared.scss
@mixin product-shared($image-size: 76) {
    width: rem($image-size);
}

// components/molecules/my-product/my-product.scss
@include product-shared();
```

Alternatively, move the variable declaration into the shared closure (for example, a `styles/settings/` partial), where the mixin body can see it.

## Optional: remove the Sass `@import` deprecation warnings

Most of the remaining deprecation warnings come from `@import` rules in the project `styles/` layer. The biggest source is the project `shared.scss` wrapper: it is loaded into every compiled component, so each of its `@import` lines warns once per compilation. To remove the warnings, we recommend migrating the project `styles/` layer from `@import` to the Sass module system—`@use` and `@forward`, supported since Dart Sass 1.23.0 (the `sass-embedded` version required by the builder includes it). Component files stay untouched: the builder injects the shared context into them, so only the `styles/` layer migrates.

The format—the wrapper forwards the core shared stylesheet and the project partials:

```scss
// src/Pyz/Yves/ShopUi/Theme/default/styles/shared.scss
@use &apos;settings/color&apos; as setting-color;

@forward &apos;~ShopUi/styles/shared&apos; with (
    $setting-color-main: setting-color.$setting-color-main,
    $setting-color-alt: setting-color.$setting-color-alt
);

@forward &apos;settings/color&apos; hide $setting-color-main, $setting-color-alt;
@forward &apos;helpers/my-helper&apos;;
```

```scss
// src/Pyz/Yves/ShopUi/Theme/default/styles/helpers/_my-helper.scss
@use &apos;../settings/color&apos; as *;

@mixin helper-my-helper {
    color: $setting-color-main;
}
```

Two rules make the migration safe:

- **Project overrides of core settings move into the `with (...)` configuration** of the core forward, and the same names are hidden from the project partial&apos;s forward. Forwarding two modules that define the same member is an error, and hiding the core member instead of configuring it would silently disconnect the project value from the core helpers that read it—the core `!default` settings exist exactly for this configuration.
- **Every partial declares its own `@use` dependencies.** In a module, a mixin resolves variables and helper mixins lexically—in the stylesheet that defines it—so a partial that references settings or other helpers must load them itself.

{% info_block warningBox &quot;Tooling that loads the wrapper with @import&quot; %}

A stylesheet that contains `@use` cannot be loaded with `@import`. If any custom tooling (for example, a Storybook Sass pipeline) prepends `@import &quot;&lt;path to shared.scss&gt;&quot;`, change it to `@use &quot;&lt;path to shared.scss&gt;&quot; as *;` placed before any other injected rules.

{% endinfo_block %}

## Storefront modules released with builder v2

The storefront modules were released together with the builder—`spryker-shop/shop-ui` as a major version, all the others as minor versions that allow ShopUi 2 in their constraints. The update command in [step 1](#1-update-composer-packages) picks them up automatically.

Modules that ship component styles received updated styles: base declarations are emitted before nested rules (the Sass `mixed-decls` fix), component styles follow the new entry-point contract, and component mixins expose [base hooks](/docs/dg/dev/frontend-development/latest/yves/atomic-frontend/managing-components/extending-components.html#extend-base-styles-with-a-base-hook). Updating them:

- removes the Sass `mixed-decls` deprecation warnings coming from the modules&apos; styles;
- removes the legacy style rescue warnings for components written against the old builder contract;
- makes base hooks available in the modules&apos; components.

The remaining modules only widen their ShopUi constraint.

&lt;details&gt;&lt;summary&gt;Module versions released with builder v2&lt;/summary&gt;

| Module | Minimum version |
| --- | --- |
| `spryker-shop/shop-ui` | `^2.0.0` |
| `spryker/multi-factor-auth` | `^2.6.0` |
| `spryker-feature/ai-commerce` | `^0.7.8` |
| `spryker-feature/buy-box` | `^1.4.0` |
| `spryker-feature/order-experience-management` | `^0.2.0` |
| `spryker-feature/purchasing-control` | `^1.2.0` |
| `spryker-feature/self-service-portal` | `^20.12.0` |
| `spryker-shop/agent-page` | `^1.25.0` |
| `spryker-shop/agent-widget` | `^1.4.0` |
| `spryker-shop/availability-widget` | `^1.5.0` |
| `spryker-shop/barcode-widget` | `^1.1.0` |
| `spryker-shop/business-on-behalf-widget` | `^1.3.0` |
| `spryker-shop/calculation-page` | `^1.4.0` |
| `spryker-shop/cart-note-widget` | `^1.7.0` |
| `spryker-shop/cart-page` | `^3.60.0` |
| `spryker-shop/cart-reorder-page` | `^1.2.0` |
| `spryker-shop/catalog-page` | `^1.37.0` |
| `spryker-shop/category-image-storage-widget` | `^1.1.0` |
| `spryker-shop/category-widget` | `^1.6.0` |
| `spryker-shop/checkout-page` | `^3.43.0` |
| `spryker-shop/checkout-widget` | `^1.5.0` |
| `spryker-shop/click-and-collect-page-example` | `^0.4.0` |
| `spryker-shop/cms-block-widget` | `^2.5.0` |
| `spryker-shop/cms-page` | `^1.9.0` |
| `spryker-shop/cms-search-page` | `^1.6.0` |
| `spryker-shop/comment-widget` | `^1.5.0` |
| `spryker-shop/company-page` | `^2.37.0` |
| `spryker-shop/company-user-agent-widget` | `^1.2.0` |
| `spryker-shop/company-user-invitation-page` | `^2.6.0` |
| `spryker-shop/company-widget` | `^1.11.0` |
| `spryker-shop/configurable-bundle-note-widget` | `^1.2.0` |
| `spryker-shop/configurable-bundle-page` | `^1.5.0` |
| `spryker-shop/configurable-bundle-widget` | `^1.10.0` |
| `spryker-shop/content-navigation-widget` | `^1.7.0` |
| `spryker-shop/content-product-widget` | `^1.5.0` |
| `spryker-shop/currency-widget` | `^1.7.0` |
| `spryker-shop/customer-page` | `^2.83.0` |
| `spryker-shop/customer-reorder-widget` | `^6.18.0` |
| `spryker-shop/date-time-configurator-page-example` | `^0.8.0` |
| `spryker-shop/discount-promotion-widget` | `^3.8.0` |
| `spryker-shop/discount-widget` | `^1.10.0` |
| `spryker-shop/error-page` | `^1.12.0` |
| `spryker-shop/file-manager-widget` | `^2.2.0` |
| `spryker-shop/gift-card-widget` | `^1.3.0` |
| `spryker-shop/home-page` | `^1.3.0` |
| `spryker-shop/language-switcher-widget` | `^1.9.0` |
| `spryker-shop/merchant-product-offer-widget` | `^2.9.0` |
| `spryker-shop/merchant-product-widget` | `^1.8.0` |
| `spryker-shop/merchant-profile-widget` | `^1.3.0` |
| `spryker-shop/merchant-registration-request-page` | `^1.1.0` |
| `spryker-shop/merchant-relation-request-page` | `^1.3.0` |
| `spryker-shop/merchant-relation-request-widget` | `^1.1.0` |
| `spryker-shop/merchant-relationship-page` | `^1.1.0` |
| `spryker-shop/merchant-relationship-widget` | `^1.1.0` |
| `spryker-shop/merchant-sales-return-widget` | `^1.2.0` |
| `spryker-shop/merchant-search-widget` | `^1.1.0` |
| `spryker-shop/merchant-switcher-widget` | `^0.9.0` |
| `spryker-shop/merchant-widget` | `^1.6.0` |
| `spryker-shop/money-widget` | `^1.8.0` |
| `spryker-shop/multi-cart-page` | `^2.9.0` |
| `spryker-shop/multi-cart-widget` | `^1.11.0` |
| `spryker-shop/newsletter-page` | `^1.3.0` |
| `spryker-shop/newsletter-widget` | `^1.9.0` |
| `spryker-shop/order-cancel-widget` | `^1.2.0` |
| `spryker-shop/order-custom-reference-widget` | `^1.2.0` |
| `spryker-shop/payment-app-widget` | `^1.4.0` |
| `spryker-shop/persistent-cart-share-widget` | `^1.4.0` |
| `spryker-shop/price-product-volume-widget` | `^1.10.0` |
| `spryker-shop/price-widget` | `^1.5.0` |
| `spryker-shop/product-alternative-widget` | `^1.7.0` |
| `spryker-shop/product-barcode-widget` | `^1.2.0` |
| `spryker-shop/product-bundle-widget` | `^1.9.0` |
| `spryker-shop/product-category-widget` | `^1.10.0` |
| `spryker-shop/product-comparison-page` | `^1.1.0` |
| `spryker-shop/product-comparison-widget` | `^1.1.0` |
| `spryker-shop/product-configuration-cart-widget` | `^1.1.0` |
| `spryker-shop/product-configuration-shopping-list-widget` | `^1.1.0` |
| `spryker-shop/product-configuration-widget` | `^1.1.0` |
| `spryker-shop/product-configuration-wishlist-widget` | `^1.1.0` |
| `spryker-shop/product-detail-page` | `^3.33.0` |
| `spryker-shop/product-group-widget` | `^1.13.0` |
| `spryker-shop/product-image-widget` | `^1.1.0` |
| `spryker-shop/product-label-widget` | `^1.7.0` |
| `spryker-shop/product-measurement-unit-widget` | `^1.5.0` |
| `spryker-shop/product-new-page` | `^1.5.0` |
| `spryker-shop/product-offer-service-point-availability-widget` | `^1.3.0` |
| `spryker-shop/product-option-widget` | `^1.6.0` |
| `spryker-shop/product-packaging-unit-widget` | `^1.9.0` |
| `spryker-shop/product-relation-widget` | `^1.5.0` |
| `spryker-shop/product-replacement-for-widget` | `^1.8.0` |
| `spryker-shop/product-review-widget` | `^1.20.0` |
| `spryker-shop/product-search-widget` | `^3.8.0` |
| `spryker-shop/product-set-detail-page` | `^1.12.0` |
| `spryker-shop/product-set-list-page` | `^1.3.0` |
| `spryker-shop/product-set-widget` | `^1.11.0` |
| `spryker-shop/product-widget` | `^1.7.0` |
| `spryker-shop/quick-order-page` | `^4.15.0` |
| `spryker-shop/quote-approval-widget` | `^1.6.0` |
| `spryker-shop/quote-request-agent-page` | `^3.6.0` |
| `spryker-shop/quote-request-agent-widget` | `^2.7.0` |
| `spryker-shop/quote-request-page` | `^3.7.0` |
| `spryker-shop/quote-request-widget` | `^2.6.0` |
| `spryker-shop/sales-configurable-bundle-widget` | `^1.7.0` |
| `spryker-shop/sales-order-amendment-widget` | `^1.2.0` |
| `spryker-shop/sales-order-threshold-widget` | `^1.2.0` |
| `spryker-shop/sales-product-bundle-widget` | `^1.3.0` |
| `spryker-shop/sales-product-configuration-widget` | `^1.2.0` |
| `spryker-shop/sales-return-page` | `^1.12.0` |
| `spryker-shop/sales-service-point-widget` | `^1.3.0` |
| `spryker-shop/service-point-widget` | `^1.8.0` |
| `spryker-shop/shared-cart-page` | `^2.6.0` |
| `spryker-shop/shared-cart-widget` | `^1.8.0` |
| `spryker-shop/shipment-type-widget` | `^1.6.0` |
| `spryker-shop/shopping-list-note-widget` | `^1.2.0` |
| `spryker-shop/shopping-list-page` | `^1.11.0` |
| `spryker-shop/shopping-list-widget` | `^1.7.0` |
| `spryker-shop/tabs-widget` | `^1.1.0` |
| `spryker-shop/traceable-event-widget` | `^1.3.0` |
| `spryker-shop/wishlist-page` | `^1.15.0` |
| `spryker-shop/wishlist-widget` | `^1.4.0` |

&lt;/details&gt;

## Post-upgrade notes

- **Sass deprecation warnings are no longer silenced.** The v1 builder suppressed warnings from dependencies; builder v2 shows them and points at the real file. Fix them in your project code instead of suppressing—they become hard errors in future Sass versions. See the base hook mechanism in [Extending components](/docs/dg/dev/frontend-development/latest/yves/atomic-frontend/managing-components/extending-components.html#extend-base-styles-with-a-base-hook) for the recommended way to extend core component base styles without triggering the `mixed-decls` deprecation.
- **Legacy style rescue.** Component SCSS files that emit CSS at the top level without being imported from a component entry point are still compiled, with a warning naming the file. Migrate such components by importing their styles from the component&apos;s `index.ts`.
- **Live reload.** `npm run yves:watch` now includes live reload: CSS changes are applied without a page reload, and JavaScript and Twig changes trigger a full reload that preserves scroll position and form state. No extra setup is needed.
- **Type checking.** From ShopUi 2.3.0, `npm run yves:lint` also runs the TypeScript compiler over the Yves TypeScript sources. It is off by default; set `typecheck: true` in `frontend/yves.settings.mts` to enable it. See [Type checking](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2.html#type-checking).
- **Lint scope.** From ShopUi 2.1.0, `npm run yves:lint` and `npm run yves:stylelint` report on the project sources. The core, eco, and feature sources are installed under `vendor/`, so both commands cover only the project sources — `src/Pyz/Yves` and every namespace in `src/` registered in `paths.sources` of `frontend/yves.settings.mts`. See [What lint covers](/docs/dg/dev/frontend-development/latest/yves/frontend-builder-for-yves-v2.html#what-lint-covers).

## Example migration

For a complete, real-world application of this guide, see the B2B Demo Marketplace migration pull request: [CC-37183 Move YVES builder into ShopUi](https://github.com/spryker-shop/b2b-demo-marketplace/pull/1282). It covers every step of this document—the composer update, `package.json` and Node.js changes, the legacy builder removal, and the minimum project style adjustments.
</description>
            <pubDate>Thu, 01 Oct 2026 11:23:30 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/upgrade-to-frontend-builder-v2-for-yves.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/upgrade-to-frontend-builder-v2-for-yves.html</guid>
            
            
        </item>
        
        <item>
            <title>Upgrade to frontend builder v2 for the Merchant Portal</title>
            <description>This document provides instructions for moving a project from the Merchant Portal build tooling in its `frontend/merchant-portal/` directory — builder v1 — to builder v2, which ships inside the ZedUi module.

For an overview of the builder, see [Frontend builder for the Merchant Portal v2](/docs/dg/dev/frontend-development/latest/marketplace/frontend-builder-for-merchant-portal-v2.html).

*Estimated migration time: 1h*

## Prerequisites

- Node.js 24.15.0 or later, npm 10 or later.
- `spryker/zed-ui` 4.3.0 or later, together with the Merchant Portal modules that allow it.
- Angular 22, or Angular 20 with the `@spryker/*` packages pinned — see [Staying on Angular 20](#staying-on-angular-20).

{% info_block warningBox &quot;Node.js 25 is not supported&quot; %}

Angular 22 accepts `^22.22.3 || ^24.15.0 || &gt;=26.0.0`. Node.js 25 satisfies a `&gt;=24.15.0` range but is not supported.

{% endinfo_block %}

## 1) Update composer packages

ZedUi 4.3.0 declares the whole npm dependency set of the Merchant Portal, and the modules that no longer declare npm dependencies themselves require it. Update ZedUi together with the Merchant Portal modules:

```bash
composer require spryker/zed-ui:&quot;^4.3.0&quot; --no-update
composer update spryker/zed-ui &quot;spryker/*-merchant-portal-gui&quot; spryker/gui-table --with-dependencies
```

Updating `spryker/zed-ui` on its own fails or leaves the project in a broken state: the locked Merchant Portal modules constrain ZedUi to an older minor, and composer does not update packages that are not listed in the update command.

## 2) Update Node.js and npm versions

1. In all `deploy.*.yml` files used by the frontend, set the versions:

```yaml
image:
    ...
    node:
        version: 24
        npm: 10
```

2. If the project has an `.nvmrc` file, update it:

```text
24
```

## 3) Update `package.json`

1. Update the engines:

```json
&quot;engines&quot;: {
    &quot;node&quot;: &quot;&gt;=24.15.0&quot;,
    &quot;npm&quot;: &quot;&gt;=10.0.0&quot;
}
```

2. Declare the ZedUi module as an npm workspace, so that npm installs the builder&apos;s dependencies:

```json
&quot;workspaces&quot;: [
    &quot;vendor/spryker/zed-ui&quot;
]
```

For what this does and how it changes the commands, see [npm workspaces for the frontend builders](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html).

3. Point the `mp:*` scripts at the `mp-zed-ui` workspace, which is the ZedUi module:

```json
&quot;scripts&quot;: {
    &quot;mp:build&quot;: &quot;npm run build -w mp-zed-ui --&quot;,
    &quot;mp:build:watch&quot;: &quot;npm run build:watch -w mp-zed-ui --&quot;,
    &quot;mp:build:production&quot;: &quot;npm run build:production -w mp-zed-ui --&quot;,
    &quot;mp:test&quot;: &quot;npm run test -w mp-zed-ui --&quot;,
    &quot;mp:lint&quot;: &quot;npm run lint -w mp-zed-ui --&quot;,
    &quot;mp:stylelint&quot;: &quot;npm run stylelint -w mp-zed-ui --&quot;,
    &quot;mp:update:config&quot;: &quot;npm run update:config -w mp-zed-ui --&quot;,
    &quot;postinstall&quot;: &quot;npm run update:config -w mp-zed-ui&quot;
}
```

4. Remove the Merchant Portal dependencies that ZedUi declares now, and keep the ones it declares as peer dependencies. A duplicate declaration in the project pins a second version of the same package, which is how two Angular or two ng-zorro copies end up in one build.

`vendor/spryker/zed-ui/package.json` is the source of truth. Its `dependencies` and `devDependencies` — Angular, ng-zorro, `@spryker/*`, the Angular builders and CLI, `jest-preset-angular` — come with the module and go from your `package.json`. Its `peerDependencies` stay with the project: in ZedUi 4.3.0 they are `@jest/globals`, `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser`, `stylelint`, `ts-jest`, `typescript`, and `webpack`. From ZedUi 4.4.0, the module declares these packages itself and has no peer dependencies, so they go from your `package.json` as well.

Everything below is installed automatically through the `mp-zed-ui` workspace and goes from your `package.json`:

- Toolchain: `@angular-builders/custom-webpack`, `@angular-builders/jest`, `@angular-devkit/build-angular`, `@angular-eslint/builder`, `@angular-eslint/eslint-plugin`, `@angular-eslint/eslint-plugin-template`, `@angular-eslint/template-parser`, `@angular/cli`, `@angular/compiler-cli`, `@angular/language-service`, `@types/jest`, `@types/node`, `angular-eslint`, `commander`, `fast-glob`, `jest-environment-jsdom`, `jest-preset-angular`, `postcss-less`, `stylelint-config-standard-less` and, from ZedUi 4.4.0, `@jest/globals`, `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser`, `stylelint`, `ts-jest`, `typescript`, `typescript-eslint`, `webpack`.
- Runtime libraries of the Merchant Portal: every `@angular/*` package, every `@spryker/*` UI package, `ng-zorro-antd`, `reflect-metadata`, `rxjs`, `tslib`, `zone.js`. The one exception is a project that stays on Angular 20, which keeps the `@spryker/*` packages pinned — see [Staying on Angular 20](#staying-on-angular-20).

For the full list, see [What you can remove from your package.json](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html#what-you-can-remove-from-your-packagejson).

See [Where the npm dependencies come from](/docs/dg/dev/frontend-development/latest/npm-workspaces-for-frontend-builders.html#where-the-npm-dependencies-come-from) for how to check what an installed package comes from.

## 4) Delete the project build tooling

1. Delete `frontend/merchant-portal/` in full. Every file in it now lives inside the ZedUi module:

```text
frontend/merchant-portal/entry-points.js
frontend/merchant-portal/html-transform.js
frontend/merchant-portal/jest.config.ts
frontend/merchant-portal/jest.preset.js
frontend/merchant-portal/mp-paths.js
frontend/merchant-portal/stylelint.mjs
frontend/merchant-portal/test-setup.ts
frontend/merchant-portal/tsconfig.spec.json
frontend/merchant-portal/update-config-paths.js
frontend/merchant-portal/utils.js
frontend/merchant-portal/webpack.config.ts
```

There is no shim: nothing in the project may keep pointing at these files. `angular.json` is repointed at the module in the next step.

2. Delete the per-module `package.json` files under your own Merchant Portal modules in `src/Pyz/Zed/*/Presentation/Components/`. ZedUi owns the dependency set, and npm workspace hoisting resolves it.

3. If the project keeps its own Merchant Portal `jest.config.*`, `test-setup.ts`, or `stylelint` runner outside `frontend/merchant-portal/`, delete those too — the packaged ones are used through `angular.json`.

{% info_block infoBox &quot;There is no automatic migration command&quot; %}

The migration is the steps in this document. ZedUi ships no project migration tool, and nothing in the module rewrites the project tree for you — apart from `mp:update:config`, which reconciles `angular.json` and the TypeScript configurations described below.

{% endinfo_block %}

## 5) Generate the configuration

Run the reconciliation:

```bash
npm run mp:update:config
```

It repoints `angular.json` at the module: the builder entry points, the Jest configuration, `outputPath` and `baseHref`, the core asset roots and the core stylesheet, and `test.options.zoneless: false`. Your `index`, `main`, `polyfills`, budgets, and optimization settings are left alone. The three `tsconfig.mp*.json` files are written inside the builder directory, unless the project already keeps a file of that name in its root.

Keep `angular.json` at the project root — the Angular CLI locates it only by walking up from the working directory — and commit it. For the full ownership split, see [Generated configuration](/docs/dg/dev/frontend-development/latest/marketplace/frontend-builder-for-merchant-portal-v2.html#generated-configuration).

`postinstall` runs the same command, so a plain `npm install` keeps the configuration current afterwards.

## 6) Update TypeScript

The builder requires TypeScript 6. In the project&apos;s `tsconfig.base.json`:

1. Set `moduleResolution` to `bundler`.
2. Remove `baseUrl`.
3. Prefix every `paths` value with `./`, including the catch-all entry:

```json
&quot;compilerOptions&quot;: {
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;paths&quot;: {
        &quot;*&quot;: [&quot;./*&quot;]
    },
    &quot;strict&quot;: false
}
```

4. Set `&quot;strict&quot;: false` explicitly if the project relies on it — TypeScript 6 defaults it to `true`.

Without the `./` prefixes, the compiler fails with `TS5090: Non-relative paths are not allowed when &apos;baseUrl&apos; is not set`.

The `paths` in the three `tsconfig.mp*.json` files are generated, so you never edit those by hand.

## 7) Install and build

```bash
npm install
npm run mp:build
```

Then verify the rest of the toolchain:

```bash
npm run mp:test
npm run mp:lint
npm run mp:stylelint
```

All three cover the project modules only — `src/Pyz/Zed` and any directory registered in `frontend/merchant-portal.settings.mts`: the core modules arrive in `vendor/` and are not the project&apos;s to report on.

Finally, check the Merchant Portal in the browser at `$[local_domain]/security-merchant-portal-gui/login`.

## Staying on Angular 20

ZedUi accepts Angular and ng-zorro `&gt;=20.3.0 &lt;23.0.0`, and the `@spryker/*` packages declare their ranges as `^old || ^new`, so the builder can be adopted before the Angular upgrade.

npm does not backtrack on peer conflicts — it selects the highest version satisfying a range and then fails if the peers do not line up — so an unpinned install on Angular 20 can resolve the new major and abort with `ERESOLVE`. Pin each `@spryker/*` package to its old major explicitly. A blanket `^3` is **wrong**: `actions.confirmation`, `datasource.dependable`, `datasource.trigger`, `datasource.trigger.change`, `datasource.trigger.input`, and `table.column.button-action` are on the `^2.x` line.

## Post-upgrade notes

- `npm run mp:build:watch` now reloads the open Back Office page after a `.ts` or `.less` change. Twig templates are not watched, because Zed caches them server-side.
- A core module added to or removed from the project changes the generated `@mp/*` aliases and `include` globs. `postinstall` regenerates them; commit the result together with the composer update.
- Project-level lint configuration lives at the project root: `eslint.config.mp.mjs` and `.stylelintrc.mp.js`. Without them, the configurations shipped in ZedUi are used.
</description>
            <pubDate>Thu, 01 Oct 2026 11:23:30 +0000</pubDate>
            <link>https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/upgrade-to-frontend-builder-v2-for-merchant-portal.html</link>
            <guid isPermaLink="true">https://docs.spryker.com/docs/dg/dev/upgrade-and-migrate/upgrade-to-frontend-builder-v2-for-merchant-portal.html</guid>
            
            
        </item>
        
    </channel>
</rss>
