Verified on Shopware 6.7
Shopware's Flow Builder is usually introduced as a way to configure what happens after an order is placed - send a confirmation email, add a tag, notify the warehouse. What's less obvious is that you can define your own trigger events, dispatched from anywhere in your plugin code, and have them appear in the same admin UI, wired to the same email template system. No custom notification infrastructure required.
The practical use case is operational alerting. API integrations that fail. Retry counters that breach a threshold. Data integrity problems that need a developer's attention before a customer notices. With a custom flow event, you get configurable recipients, Twig-templated emails with dynamic variables, and flow conditions - all without touching notification code after the initial implementation.
What a Flow Business Event Needs
A flow event is a standard Symfony Event subclass with Shopware-specific interfaces bolted on. The minimum for a useful alerting event:
<?php declare(strict_types=1);
namespace Acme\Plugin\Event;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\Content\Flow\Dispatching\Aware\ScalarValuesAware;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Event\CustomerAware;
use Shopware\Core\Framework\Event\EventData\ArrayType;
use Shopware\Core\Framework\Event\EventData\EventDataCollection;
use Shopware\Core\Framework\Event\EventData\ScalarValueType;
use Shopware\Core\Framework\Event\FlowEventAware;
use Shopware\Core\Framework\Event\MailAware;
use Shopware\Core\Framework\Event\MailRecipientStruct;
use Shopware\Core\Framework\Event\SalesChannelAware;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Contracts\EventDispatcher\Event;
class ExternalApiFailedEvent extends Event implements
FlowEventAware,
MailAware,
CustomerAware,
SalesChannelAware,
ScalarValuesAware
{
public const EVENT_NAME = 'Acme.ExternalApi.Failed';
public function __construct(
private readonly CustomerEntity $customer,
private readonly SalesChannelContext $salesChannelContext,
private readonly string $errorMessage,
private readonly int $errorCode,
private readonly string $requestPayload
) {}
public static function getName(): string
{
return self::EVENT_NAME;
}
// FlowEventAware - declares what data this event exposes
public static function getAvailableData(): EventDataCollection
{
return (new EventDataCollection())
->add('data', new ArrayType(new ScalarValueType(ScalarValueType::TYPE_STRING)));
}
// ScalarValuesAware - values injected as Twig variables in mail templates
public function getValues(): array
{
return [
'errorMessage' => $this->errorMessage,
'errorCode' => (string) $this->errorCode,
'requestPayload' => $this->requestPayload,
];
}
// MailAware - provides recipient list for the "Send email" flow action
public function getMailStruct(): MailRecipientStruct
{
return new MailRecipientStruct([
$this->customer->getEmail() => $this->customer->getFirstName() . ' ' . $this->customer->getLastName(),
]);
}
// CustomerAware
public function getCustomerId(): string
{
return $this->customer->getId();
}
// SalesChannelAware
public function getSalesChannelId(): string
{
return $this->salesChannelContext->getSalesChannelId();
}
public function getContext(): Context
{
return $this->salesChannelContext->getContext();
}
}
FlowEventAware - marks the class as a flow trigger. Without this interface the event is a plain Symfony event and never appears in Flow Builder.
MailAware - enables the "Send email" action in flows triggered by this event. getMailStruct() provides the default recipient. If recipients should always be defined in Flow Builder rather than in code, return new MailRecipientStruct([]) and let the admin configure them in the action.
ScalarValuesAware + getValues() - exposes values as Twig variables in mail templates. Keys returned from getValues() are available directly as {{ errorMessage }}, {{ errorCode }}, {{ requestPayload }}. Use (string) casts on non-string values - the type must match ScalarValueType::TYPE_STRING.
CustomerAware and OrderAware are optional. Include them when the failure is tied to a customer or order - they unlock additional flow conditions and expose the entity's fields to templates.
Step 2: The Registration Subscriber
This is the step most tutorials skip entirely. Without it, the event never appears in the Flow Builder trigger dropdown, even if it dispatches correctly.
<?php declare(strict_types=1);
namespace Acme\Plugin\Subscriber;
use Acme\Plugin\Event\ExternalApiFailedEvent;
use Shopware\Core\Framework\Event\BusinessEventCollectorEvent;
use Shopware\Core\Framework\Event\BusinessEventDefinition;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class BusinessEventCollectorSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
BusinessEventCollectorEvent::NAME => 'onCollect',
];
}
public function onCollect(BusinessEventCollectorEvent $event): void
{
$event->set(
ExternalApiFailedEvent::EVENT_NAME,
new BusinessEventDefinition(
ExternalApiFailedEvent::EVENT_NAME,
ExternalApiFailedEvent::getAvailableData()
)
);
}
}
Register it in services.xml:
<service id="Acme\Plugin\Subscriber\BusinessEventCollectorSubscriber">
<tag name="kernel.event_subscriber"/>
</service>
One subscriber per plugin handles all events in that plugin - call $event->set() once for each event class.
Step 3: Dispatching from a Service
class ExternalApiService
{
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly HttpClientInterface $client
) {}
public function syncData(
CustomerEntity $customer,
array $payload,
SalesChannelContext $context
): ResponseData {
try {
return $this->client->post('/sync', $payload);
} catch (\Exception $e) {
$this->eventDispatcher->dispatch(
new ExternalApiFailedEvent(
customer: $customer,
salesChannelContext: $context,
errorMessage: $e->getMessage(),
errorCode: $e->getCode(),
requestPayload: json_encode($payload)
),
ExternalApiFailedEvent::EVENT_NAME
);
throw $e;
}
}
}
The second argument to dispatch() is the event name string. Shopware's flow dispatcher routes events by name, so it must match EVENT_NAME exactly.
Step 4: Wiring It in Flow Builder
After clearing the cache (bin/console cache:clear), open Marketing > Flow Builder > Create flow. The event appears in the trigger dropdown under its name. A basic alert flow:
- Set trigger to
Acme.ExternalApi.Failed - Add a condition if needed - only notify for specific error codes, or only during business hours
- Add a "Send email" action and assign a mail template
- In the mail template body, reference the scalar variables directly:
{{ errorMessage }},{{ errorCode }},{{ requestPayload }}
No code change is needed to adjust recipients, swap templates, or add conditions. An admin or developer can modify the notification behavior without a deployment.
Two Recipient Strategies
Customer-context events - the failure is tied to a specific customer session (a payment API rejected their transaction, a credit balance fetch failed). Implement CustomerAware, populate getMailStruct() with the customer's email. An ops address can be added via the Flow Builder action.
System-level events - the failure is an internal data integrity issue, not tied to any particular customer. Return new MailRecipientStruct([]) from getMailStruct(). Define the recipient (your ops email address) in the Flow Builder action instead. This keeps hardcoded addresses out of code and lets the address be changed without a release.
Escalation: A Second Event at a Retry Threshold
A common pattern is two events at different severity levels: one dispatched on first failure, a different one dispatched when a retry counter reaches a configured threshold. Two event names, two independent flows, separate templates and recipients.
public function processRetry(FailedRecord $record, Context $context): void
{
$result = $this->attemptExternalCall($record);
if (!$result->isSuccessful()) {
$record->incrementRetryCount();
$this->repository->update($record, $context);
$threshold = (int) $this->config->get('Acme.config.retriesBeforeEscalation');
if ($record->getRetryCount() >= $threshold) {
$this->eventDispatcher->dispatch(
new ExternalApiEscalatedEvent($record, $context),
ExternalApiEscalatedEvent::EVENT_NAME
);
}
}
}
ExternalApiEscalatedEvent is a second event class, registered separately in the same BusinessEventCollectorSubscriber. In Flow Builder it appears as a distinct trigger. The "failed" flow might log or send a low-priority notification; the "escalated" flow notifies a developer mailbox or a monitoring system via a different template. Both are fully configurable without touching application code.