Events & Flow Builder
The extension dispatches exactly one custom event: a terminal job notification that you can react to from the Flow Builder or from PHP. It also registers two scheduled tasks and two subscribers.
Flow Builder trigger
Nice\Translate\Job\Event\TranslationJobFinishedEvent is a registered business event, therefore it appears in the trigger list of the Flow Builder.
| Aspect | Value |
|---|---|
| Technical name | nice_translate.job.finished |
| Label in the Administration | Translation job finished |
| Trigger path | AI Translation → Job → Finished |
| Class | Nice\Translate\Job\Event\TranslationJobFinishedEvent |
| Base class | Symfony\Contracts\EventDispatcher\Event |
It implements three aware interfaces:
| Interface | Consequence |
|---|---|
Shopware\Core\Framework\Event\FlowEventAware | The event can be a flow trigger at all |
Shopware\Core\Content\Flow\Dispatching\Aware\ScalarValuesAware | The extension persists the six payload values with the flow sequence, therefore a storable, deferred flow execution still has them |
Shopware\Core\Framework\Event\MailAware | The mail action of Shopware can address the creator of the job, and you configure no recipient |
getSalesChannelId() always returns null. Translation jobs are administration-level work without a sales channel scope.

Payload
getAvailableData() exposes exactly these six scalars, in this order. getValues() returns the same six keys, which makes them available to a deferred flow execution and to a mail template.
| Property | Type | Meaning |
|---|---|---|
jobId | string | The hex UUID of the nice_translate_job row |
title | string | The job title, as the job list shows it |
status | string | The terminal status that the job reached |
processedItems | int | The records that the extension translated |
failedItems | int | The records that failed |
skippedItems | int | The records that the safety gates skipped |
The extension finalises a job after processedItems + failedItems + skippedItems reaches the planned total. The three counters together therefore account for the full run. A record counts as skipped when a safety gate refused to overwrite it. See Safety mechanisms.
When it fires
The event carries one of three statuses:
| Status | Reached when |
|---|---|
completed | The extension processed each record, and none failed |
completed_with_errors | The job finished with a minimum of one failed record |
failed | The preparation of a queued job failed before any batch ran |
The batch finaliser produces the first two statuses only. failed arrives only through JobProgressUpdater::failQueued(), which writes its own snapshot to the same outbox.
A cancelled job emits no event. On-save automation also emits none, because it translates without a job. See Automation.
The finalizer writes the terminal snapshot into the nice_translate_finished_event outbox, in the same transaction that finalises the job, and it never dispatches the event itself. JobFinishedEventRelay collects the snapshot after that commit. The delivery is therefore at-least-once: a dispatch that throws keeps its lease, and the scheduled recovery task retries it after the stale window. Deduplicate on jobId if your listener has side effects.
Mail recipients
The relay derives the recipient from the creator of the job, that is, the Shopware user that nice_translate_job.created_by_id refers to. That address travels with the event, which lets a Flow Send mail action reach the creator without further configuration.
WARNING
The scheduled task creates its jobs under a system context, and those jobs have no creator. The recipient list is therefore empty for them. Configure an explicit recipient in your mail action if you want a notification about the scheduled runs.
Build a flow
These steps build a flow that reports the jobs that finished with errors:
- In the Administration, open the Flow Builder and create a new flow. Give it a name such as Translation job finished.
- As the trigger, select AI Translation → Job → Finished. The list shows it as Translation job finished.
- Add a mail action. The event is
MailAware, therefore the action can address the creator of the job. It also accepts all administrators, or a fixed address. - In the mail template, branch on the payload, so that a clean run sends no mail:
{% if failedItems > 0 %}
Translation job "{{ title }}" finished as {{ status }}.
{{ processedItems }} processed, {{ failedItems }} failed, {{ skippedItems }} skipped.
Open the job detail view for the error list: job id {{ jobId }}.
{% endif %}The trigger fires for each job that reaches a terminal status, and it has no filter of its own. The condition therefore belongs in the template, or in a PHP listener.
Scheduled tasks
The extension registers two scheduled tasks with the shopware.scheduled.task tag. Both need an active bin/console scheduled-task:run worker.
| Task class | Task name | Default interval |
|---|---|---|
AutoTranslateTask | nice_translate.auto_translate | 3600 s (1 hour) |
BatchDispatchTask | nice_translate.dispatch_batches | 60 s (1 minute) |
nice_translate.auto_translate
The task ticks hourly and translates a maximum of once per scheduledIntervalHours. AutoTranslateTaskHandler::run() performs these steps on each tick, in order:
- Prune the history. It deletes the
nice_translate_historyrows that are older thanhistoryRetentionDays. This happens on each tick, with scheduled translation on or off. For a configured value below 1, the retention falls back to 30 days. - If
scheduledEnabledis off, return. Nothing below this point operates. - If the configured interval did not pass since
scheduledLastRun, return. For a configured value below 1,scheduledIntervalHoursfalls back to 24. - If
scheduledEntitiesorautoTranslateLanguageIdsis empty, remember the run and return. - If any
nice_translate_jobis stillqueuedorrunning, return without a record of the run. The run stays due, and the next tick makes the same check again. - Group the target languages by their resolved provider, and create one job for each provider group. One job per provider keeps the validation, the accounting and the job labels unambiguous. The jobs use the scope
missing, the modemissing,skipUnchangedandprotectManualEdits, and their title starts withScheduled (<provider label>):. - Write
scheduledLastRunas an ISO-8601 timestamp, after the creation of a minimum of one job. A due run that created no job keeps the timestamp unchanged, therefore the next tick tries again.
The entity list of this task is scheduledEntities, a setting separate from the autoTranslateEntities that on-save automation reads. Automation and the Settings reference document both settings.
nice_translate.dispatch_batches
BatchDispatchTaskHandler::run() calls JobBatchDispatcher::recover(), then JobFinishedEventRelay::recover(). Each call has its own try/catch that logs the failure, therefore one failing recovery pass does not block the other pass.
This task makes the durability layer operate. It enqueues the committed jobs whose start message never reached the transport, relays the batches with an incomplete dispatch again, retries the stale finished-event claims and prunes the expired receipts. It is infrastructural and has no settings.
Subscribers
src/Job/Subscriber/ contains two subscribers, and both are registered as kernel.event_subscriber.
| Class | Subscribes to |
|---|---|
BusinessEventCollectorSubscriber | BusinessEventCollectorEvent::NAME, priority 1000 |
EntityWrittenSubscriber | product.written, category.written, product_manufacturer.written, cms_page.written |
BusinessEventCollectorSubscriber defines TranslationJobFinishedEvent and adds it to the collection, which makes the trigger appear in the Flow Builder.
EntityWrittenSubscriber implements the on-save automation. It walks a guard chain before it dispatches anything, in this order:
autoTranslateEnabledmust be true.- The context must not carry the state
nice_translate.writing. This is the loop guard, and it stops the writes of the extension from a second trigger of the subscriber. - The write must have happened in the system default language.
- The write must have happened in the live version.
autoTranslateEntitiesmust contain the entity.autoTranslateLanguageIdsmust not be empty.- The subscriber drops the delete results and deduplicates the remaining ids.
It then resolves a provider for each target language, builds a jobless JobConfig with the scope selection, the mode missing, skipUnchanged and protectManualEdits, divides the ids into batches of the configured size, and registers a durable manifest before the dispatch. If it cannot resolve a provider, it logs the failure and skips that language. It also logs a dispatch failure and then leaves the work to the recovery task.
TranslationJobFinishedEvent is the only custom event class in the extension, and the extension dispatches nothing else. If you need a hook that does not exist, the provider tag from Custom providers is usually the correct place.
Listen in PHP
The relay dispatches with an explicit event name:
$this->eventDispatcher->dispatch($event, TranslationJobFinishedEvent::EVENT_NAME);Key your subscriber on that name. A subscriber that you register against the class does not receive the dispatch:
<?php declare(strict_types=1);
namespace Acme\Translate\Subscriber;
use Nice\Translate\Job\Event\TranslationJobFinishedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class TranslationJobSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
TranslationJobFinishedEvent::EVENT_NAME => 'onJobFinished',
];
}
public function onJobFinished(TranslationJobFinishedEvent $event): void
{
if ($event->getFailedItems() === 0) {
return;
}
$this->logger->warning('Translation job finished with errors.', [
'jobId' => $event->getJobId(),
'title' => $event->getTitle(),
'status' => $event->getStatus(),
'processed' => $event->getProcessedItems(),
'failed' => $event->getFailedItems(),
'skipped' => $event->getSkippedItems(),
]);
}
}<service id="Acme\Translate\Subscriber\TranslationJobSubscriber">
<argument type="service" id="logger"/>
<tag name="kernel.event_subscriber"/>
</service>Two properties of the dispatch are important for each listener that you write:
- It operates in a queue worker. The relay dispatches from the process that finalised the job: a queue worker, or the scheduled recovery task. There is no web request around it, and
getContext()returns a context from a system source. There is therefore no administration user and no sales channel to read from it. - It can be called more than one time for the same job. The delivery is at-least-once. Keep the listener idempotent, or deduplicate on
jobId.
If you need more than the six scalars, load the nice_translate_job entity by jobId through its repository. See Admin API for the entity list and its necessary privileges.