Plenty Flow

The Flow module, commonly referred to as plentyflow, provides a powerful, event-driven Workflow Automation System for plentymarkets. It enables users to define, configure, and execute automated workflows based on a variety of triggers, actions, and filters. This significantly enhances operational efficiency by automating complex business processes across the platform, from order processing to customer communication.

Plugins register their own triggers, filters and actions through dedicated plugin-facing base classes: PluginFlowTriggerDefinition, PluginFlowFilterDefinition and PluginFlowStepActionDefinition. These classes expose only the members a plugin may implement or call; everything else that the internal definitions need (translations, config-field bookkeeping, registration plumbing, …) is locked away and handled by the monolith. All three operate on orders: a plugin trigger fires with an order, a plugin filter is evaluated against an order, and a plugin action receives and returns orders.

Registering Triggers to Flow

To register a new trigger to the flow, extend the PluginFlowTriggerDefinition abstract class and use the PluginFlowRegistrationService to register your trigger.

Step-by-Step Guide

  1. Create a New Trigger Class

    • Extend the PluginFlowTriggerDefinition abstract class.

    • Define all the required abstract methods.

      <?php
      
      namespace YourNamespace;
      
      use Plenty\Modules\Flow\Triggers\Definitions\Models\Plugin\PluginFlowTriggerDefinition;
      
      class YourCustomTrigger extends PluginFlowTriggerDefinition
      {
          public function getTriggerIdentifier(): string
          {
              return 'your_custom_trigger'; // Unique identifier for your trigger, prefix it with your plugin name
          }
      
          public function getTriggerName(): string
          {
              return 'Your Custom Trigger'; // Display name of your trigger
          }
      
          public function getTriggerDescription(): string
          {
              return 'Description of your custom trigger'; // Description of your trigger
          }
      
          public function getTriggerAIDescription(): string
          {
              return 'A description of your trigger for AI-assisted flow creation';
          }
      
          public function getUIConfigField(): array|null
          {
              return null; // Return an array of FormField objects, or null if no configuration is needed
          }
      }
  2. Register the Trigger

    • Use the PluginFlowRegistrationService to register your trigger.

    • Ensure that the registration method is called during the application bootstrapping process.

      // In your service provider or bootstrap file
      /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
      $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
      $pluginFlowRegistrationService->registerTrigger(pluginApp(YourNamespace\YourCustomTrigger::class));

Example: OrderStatusChangedTrigger

  • Build the configuration fields with getFormField() — the only sanctioned way to add fields to the config form. It registers each field internally so its value can be validated later.

  • getTriggerObject() returns the object type this trigger operates on. For the first release this is always the order object type; pass it as the $object argument to PluginFlowRegistrationService::fireTrigger().

    <?php
    
    namespace YourNamespace;
    
    use App;
    use Plenty\Modules\Flow\Contracts\UIConfigFormContract;
    use Plenty\Modules\Flow\DataModels\ConfigForm\CheckboxGroupField;
    use Plenty\Modules\Flow\Triggers\Definitions\Models\Plugin\PluginFlowTriggerDefinition;
    use Plenty\Modules\Order\Status\Contracts\OrderStatusRepositoryContract;
    use Plenty\Modules\Localization\Contracts\LocalizationRepositoryContract;
    
    class OrderStatusChangedTrigger extends PluginFlowTriggerDefinition
    {
        const IDENTIFIER = 'yourPlugin::orderStatusChanged';
    
        public function getTriggerIdentifier(): string
        {
            return self::IDENTIFIER;
        }
    
        public function getTriggerName(): string
        {
            return 'Order Status Changed';
        }
    
        public function getTriggerDescription(): string
        {
            return 'Triggered when the status of an order changes';
        }
    
        public function getTriggerAIDescription(): string
        {
            return 'The trigger is activated when the status of an order changes. Accepts a multi-select list of order statuses.';
        }
    
        public function getUIConfigField(): array|null
        {
            $configForm = pluginApp(UIConfigFormContract::class, [
                'translationNamespace' => 'module_flow'
            ]);
    
            $orderStatusRepository = pluginApp(OrderStatusRepositoryContract::class);
            $statuses = $orderStatusRepository->all();
    
            /** @var LocalizationRepositoryContract $localizationRepository */
            $localizationRepository = pluginApp(LocalizationRepositoryContract::class);
    
            $lang = localizationRepository->getLanguage() ?? 'de';
    
            $statusField = $this->getFormField(CheckboxGroupField::class, [
                'name' => 'statusId',
                'label' => 'config.orderStatus'
            ]);
    
            foreach ($statuses as $status) {
                $statusField->addCheckboxGroupValue(
                    $status->names[$lang] ?? (string)$status->names->first() ?? (string)$status->statusId,
                    $status->statusId,
                    false
                );
            }
    
            $configForm->addCheckboxGroupField($statusField);
    
            return $configForm->getConfigFields();
        }
    }
    1. Register the Trigger

  • Use the PluginFlowRegistrationService to register your trigger.

  • Ensure that the registration method is called during the application bootstrapping process.

    // In your service provider or bootstrap file
    /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
    $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
    $pluginFlowRegistrationService->registerTrigger(pluginApp(YourNamespace\OrderStatusChangedTrigger::class));

Explanation of PluginFlowRegistrationService::fireTrigger

The PluginFlowRegistrationService::fireTrigger method is responsible for firing triggers based on specific events. It verifies if a trigger exists and then starts the flows associated with that trigger.

Example of Firing the Order Status Change Trigger

/** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
$pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
/** @var OrderStatusChangedTrigger $orderStatusChangedTrigger */
$orderStatusChangedTrigger = pluginApp(OrderStatusChangedTrigger::class);
$pluginFlowRegistrationService->fireTrigger(
    $orderStatusChangedTrigger->getTriggerIdentifier(),
    (string)$order->id,
    'statusId',
    $order->statusId
);

Registering Branch Filters to Flow

Please note that these filters will be available only to flows' branch control elements.

To register a new branch filter to the flow, extend the PluginFlowFilterDefinition abstract class and use the PluginFlowRegistrationService to register your filter. == Step-by-Step Guide

  1. Create a New Filter Class

    • Extend the PluginFlowFilterDefinition abstract class.

    • Define all the required abstract methods.

      <?php
      
      namespace YourNamespace\Flow\Filters;
      
      use Plenty\Modules\Flow\Filters\Definitions\Models\Plugin\PluginFlowFilterDefinition;
      
      class YourCustomFilter extends PluginFlowFilterDefinition
      {
          public function getIdentifier(): string
          {
              return 'your_custom_filter'; // Unique identifier for your filter, prefix it with your plugin name
          }
      
          public function getName(): string
          {
              return 'Your Custom Filter'; // Display name of your filter
          }
      
          public function getDescription(): string
          {
              return 'Description of your custom filter';  // Description of your filter
          }
      
          public function getAIDescription(): string
          {
              return 'A description of your filter for AI-assisted flow creation';
          }
      
          public function getUIConfigFields(): array
          {
              return []; // Return an array of FormField objects
          }
      
          public function getOperators(): array
          {
              return []; // Return an array of operators eg. FilterOperators::IN, FilterOperators::NOT_IN
          }
      
          public function performFilter(array $inputs, array $filterField, array $extraParams = []): bool
          {
               // Perform filter logic here
              return true;
          }
      }
  2. Register the Filter

    • Use the PluginFlowRegistrationService to register your filter.

    • Ensure that the registration method is called during the application bootstrapping process.

      // In your service provider or bootstrap file
      /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
      $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
      $pluginFlowRegistrationService->registerFilter(pluginApp(YourNamespace\YourCustomFilter::class));

Example: StatusFilter

  • getRequiredInputTypes() and getAvailabilities() are locked to orders for plugin filters and cannot be overridden — a plugin filter is always evaluated against the order object and is always available in order branches.

  • addOperators() adds the operator selector for the values returned by getOperators(); call it from getUIConfigFields().

  • mapFilterFields() flattens the raw filter field configuration into [key ⇒ ['operator' ⇒ …, 'value' ⇒ …]]; call it at the start of performFilter().

    <?php
    
    namespace YourNamespace\Flow\Filters;
    
    use Plenty\Modules\Flow\Contracts\UIConfigFormContract;
    use Plenty\Modules\Flow\DataModels\ConfigForm\CheckboxGroupField;
    use Plenty\Modules\Flow\DataModels\ConfigForm\SelectboxField;
    use Plenty\Modules\Flow\Enums\FilterOperators;
    use Plenty\Modules\Flow\Filters\Definitions\Models\Plugin\PluginFlowFilterDefinition;
    use Plenty\Modules\Order\Contracts\OrderRepositoryContract;
    use Plenty\Modules\Order\Status\Contracts\OrderStatusRepositoryContract;
    use Plenty\Modules\Localization\Contracts\LocalizationRepositoryContract;
    
    class StatusFilter extends PluginFlowFilterDefinition
    {
        const IDENTIFIER = 'yourPlugin::filterOrderStatus';
        const KEY = 'statusId';
    
        public function getIdentifier(): string
        {
            return self::IDENTIFIER;
        }
    
        public function getName(): string
        {
            return 'Order Status Filter';
        }
    
        public function getDescription(): string
        {
            return 'Filter orders by status';
        }
    
        public function getAIDescription(): string
        {
            return 'Filters orders by their current status. Accepts one or more order statuses to compare against.';
        }
    
        public function getOperators(): array
        {
            return [
                FilterOperators::IN,
                FilterOperators::NOT_IN,
                FilterOperators::LESS_THAN,
                FilterOperators::LESS_OR_EQUAL,
                FilterOperators::GREATER_THAN,
                FilterOperators::GREATER_OR_EQUAL,
            ];
        }
    
        public function getUIConfigFields(): array
        {
            /** @var UIConfigFormContract $configForm */
            $configForm = pluginApp(
                UIConfigFormContract::class,
                [
                    'translationNamespace' => 'module_flow'
                ]
            );
            $configForm = $this->addOperators($configForm, self::KEY);
    
            /** @var LocalizationRepositoryContract $localizationRepository */
            $localizationRepository = pluginApp(LocalizationRepositoryContract::class);
    
            $lang = localizationRepository->getLanguage() ?? 'de';
    
            /** @var OrderStatusRepositoryContract $orderStatusRepository */
            $orderStatusRepository = pluginApp(OrderStatusRepositoryContract::class);
            $statuses = $orderStatusRepository->all();
    
            /** @var SelectboxField $orderStatusSelectBox */
            $orderStatusSelectBox = $this->getFormField(
                SelectboxField::class,
                [
                    'name' => self::KEY,
                    'label' => 'config.orderStatuses'
                ]
            );
    
            /** @var CheckboxGroupField $orderStatusCheckBoxGroup */
            $orderStatusCheckBoxGroup = $this->getFormField(
                CheckboxGroupField::class,
                [
                    'name' => self::KEY,
                    'label' => 'config.orderStatuses'
                ]
            );
            // Allow partial export of globally valid status IDs for system specific actions
            $orderStatusSelectBox->partialSystemSpecificValues = PartialSystemSpecificValueSource::ORDER_STATUS;
            $orderStatusCheckBoxGroup->partialSystemSpecificValues = PartialSystemSpecificValueSource::ORDER_STATUS;
    
            /** @var OrderStatus $status */
            foreach ($statuses as $status) {
                $orderStatusSelectBox->addSelectboxValue(
                    $status->names[$lang] ?? (string)$status->names->first() ?? (string)$status->statusId,
                    $status->statusId,
                    false
                );
                $orderStatusCheckBoxGroup->addCheckBoxValue(
                    $status->names[$lang] ?? (string)$status->names->first() ?? (string)$status->statusId,
                    $status->statusId,
                    false
                );
            }
            $orderStatusSelectBox->condition = 'operator != "IN" && operator != "NIN"';
            $orderStatusSelectBox->conditionKeys = ['operator'];
            $orderStatusSelectBox->setupPath = SetupPaths::ORDER_STATUS;
            $orderStatusCheckBoxGroup->condition = 'operator == "IN" || operator == "NIN"';
            $orderStatusCheckBoxGroup->conditionKeys = ['operator'];
            $orderStatusCheckBoxGroup->setupPath = SetupPaths::ORDER_STATUS;
    
            $configForm->addSelectboxField($orderStatusSelectBox, self::KEY);
            $configForm->addCheckboxGroupField($orderStatusCheckBoxGroup, self::KEY);
    
            return $configForm->getConfigFields();
        }
    
        public function performFilter(array $inputs, array $filterField, array $extraParams = []): bool
        {
            $filterField = $this->mapFilterFields($filterField);
    
            $orderId = (int)$inputs[$this->getObjectType()]->value;
            $orderRepository = pluginApp(OrderRepositoryContract::class);
            $order = $orderRepository->findById($orderId);
    
            $operator = $filterField[self::KEY]['operator'];
            $value = match ($operator) {
                FilterOperators::LESS_THAN => $order->statusId < $filterField[self::KEY]['value'],
                FilterOperators::LESS_OR_EQUAL => $order->statusId <= $filterField[self::KEY]['value'],
                FilterOperators::GREATER_THAN => $order->statusId > $filterField[self::KEY]['value'],
                FilterOperators::GREATER_OR_EQUAL => $order->statusId >= $filterField[self::KEY]['value'],
                FilterOperators::IN => in_array($order->statusId, $filterField[self::KEY]['value']),
                FilterOperators::NOT_IN => !in_array($order->statusId, $filterField[self::KEY]['value']),
                default => false,
            };
    
            $this->captureGiven(self::KEY, $order->statusId);
    
            return $value;
        }
    }
    1. Register the Filter

  • Use the PluginFlowRegistrationService to register your filter.

  • Ensure that the registration method is called during the application bootstrapping process.

    // In your service provider or bootstrap file
    /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
    $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
    $pluginFlowRegistrationService->registerFilter(pluginApp(YourNamespace\Flow\Filters\StatusFilter::class));

Registering Actions to Flow

To register a new action to the flow, extend the PluginFlowStepActionDefinition abstract class and use the PluginFlowRegistrationService to register your action.

Step-by-Step Guide

  1. Create a New Action Class

    • Extend the PluginFlowStepActionDefinition abstract class.

    • Define all the required abstract methods.

      <?php
      
      namespace YourNamespace;
      
      use Plenty\Modules\Flow\Models\Filter;
      use Plenty\Modules\Flow\Models\Output;
      use Plenty\Modules\Flow\StepActions\Definitions\Models\Plugin\PluginFlowStepActionDefinition;
      
      class YourCustomAction extends PluginFlowStepActionDefinition
      {
          public function getIdentifier(): string
          {
              return 'your_custom_action'; // Unique identifier for your action, prefix it with your plugin name
          }
      
          public function getPathIcon(): string
          {
              return 'path/to/your/icon'; // Path to your icon eg. 'shopping_cart'
          }
      
          public function getName(): string
          {
              return 'Your Custom Action'; // Display name of your action
          }
      
          public function getIcon(): string
          {
              return 'icon-name'; // Icon name for your action eg. 'shopping_cart'
          }
      
          public function getDescription(): string
          {
              return 'Description of your custom action'; // Description of your action
          }
      
          public function getAIDescription(): string
          {
              return 'A description of your action for AI-assisted flow creation';
          }
      
          public function getPluginName(): string
          {
              return 'Plugin name'; // Replace with your plugin name
          }
      
          public function getUIConfigFields(): array
          {
              return []; // Return an array of FormField objects
          }
      
          public function performTask(array $inputs, array $configFields, Filter $filter = null, array $extraParams = []): array
          {
              return []; // Perform action logic here, return one Output per object the next step should receive
          }
      }
  2. Register the Action

    • Use the PluginFlowRegistrationService to register your action.

    • Ensure that the registration method is called during the application bootstrapping process.

      // In your service provider or bootstrap file
      /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
      $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
      $pluginFlowRegistrationService->registerAction(pluginApp(YourNamespace\YourCustomAction::class));

Example: ChangeOrderStatusAction

  • performTask() receives a list of Input objects under $inputs[$this→getObjectType()] — one per order the flow is currently processing — and must return one Output per order the following step should receive. Build outputs with pluginApp(Output::class, ['name' ⇒ $this→getObjectType(), 'value' ⇒ …]); skipping an order (for example because it failed) simply means not adding an output for it. Returning an empty array stops the flow branch entirely, because the next step then gets no inputs.

  • writeHistoryInfo() and writeHistoryError() write to the flow execution history that end users see; use them to report success or failure per order.

    <?php
    
    namespace YourNamespace;
    
    use Plenty\Modules\Flow\Contracts\UIConfigFormContract;
    use Plenty\Modules\Flow\DataModels\ConfigForm\SelectboxField;
    use Plenty\Modules\Flow\Models\Filter;
    use Plenty\Modules\Flow\Models\Output;
    use Plenty\Modules\Flow\StepActions\Definitions\Models\Plugin\PluginFlowStepActionDefinition;
    use Plenty\Modules\Order\Contracts\OrderRepositoryContract;
    use Plenty\Modules\Order\Status\Contracts\OrderStatusRepositoryContract;
    use Plenty\Modules\Localization\Contracts\LocalizationRepositoryContract;
    
    class ChangeOrderStatusAction extends PluginFlowStepActionDefinition
    {
        const IDENTIFIER = 'yourPlugin::changeOrderStatus';
    
        public function getIdentifier(): string
        {
            return self::IDENTIFIER;
        }
    
        public function getPathIcon(): string
        {
            return 'shopping_cart';
        }
    
        public function getName(): string
        {
            return 'Change Order Status';
        }
    
        public function getIcon(): string
        {
            return 'shopping_cart';
        }
    
        public function getDescription(): string
        {
            return 'Changes the status of an order';
        }
    
        public function getTooltip(): string
        {
            return 'Sets the given order to the configured status';
        }
    
        public function getAIDescription(): string
        {
            return 'Sets the order to the configured status. Accepts a single order status to apply.';
        }
    
        public function getPluginName(): string
        {
            return 'Plugin name'; // Replace with your plugin name
        }
    
        public function getUIConfigFields(): array
        {
            $configForm = pluginApp(UIConfigFormContract::class, [
                'translationNamespace' => 'module_flow'
            ]);
    
            $orderStatusesSelectBox = $this->getFormField(SelectboxField::class, [
                'name' => 'statusId',
                'label' => 'config.orderStatus'
            ]);
    
            /** @var LocalizationRepositoryContract $localizationRepository */
            $localizationRepository = pluginApp(LocalizationRepositoryContract::class);
    
            $lang = localizationRepository->getLanguage() ?? 'de';
    
            $orderStatusRepository = pluginApp(OrderStatusRepositoryContract::class);
            $statuses = $orderStatusRepository->all();
    
            foreach ($statuses as $status) {
                $orderStatusesSelectBox->addSelectboxValue($status->names[$lang] ?? (string) $status->names->first() ?? (string)$status->statusId, $status->statusId, false);
            }
    
            $configForm->addSelectboxField($orderStatusesSelectBox);
    
            return $configForm->getConfigFields();
        }
    
        public function performTask(array $inputs, array $configFields, Filter $filter = null, array $extraParams = []): array
        {
            $orderRepository = pluginApp(OrderRepositoryContract::class);
    
            $outputs = [];
            foreach ($inputs[$this->getObjectType()] as $input) {
                $orderId = (int)$input->value;
    
                try {
                    $orderRepository->update($orderId, [
                        'statusId' => (float)$configFields['statusId']->value,
                    ]);
    
                    $this->writeHistoryInfo($extraParams['flowName'] ?? '', $extraParams['workflowName'] ?? null, 'Order status changed', ['orderId' => $orderId]);
                } catch (\Throwable $e) {
                    $this->writeHistoryError($extraParams['flowName'] ?? '', $extraParams['workflowName'] ?? null, $e->getMessage(), ['orderId' => $orderId]);
                    continue;
                }
    
                $outputs[] = pluginApp(Output::class, [
                    'name' => $this->getObjectType(),
                    'value' => (string)$orderId
                ]);
            }
    
            return $outputs;
        }
    }
    1. Register the Action

  • Use the PluginFlowRegistrationService to register your action.

  • Ensure that the registration method is called during the application bootstrapping process.

    // In your service provider or bootstrap file
    /** @var PluginFlowRegistrationService $pluginFlowRegistrationService */
    $pluginFlowRegistrationService = pluginApp(PluginFlowRegistrationService::class);
    $pluginFlowRegistrationService->registerAction(pluginApp(YourNamespace\ChangeOrderStatusAction::class));

Summary

By following these steps, you can create and register a custom action in flow. Ensure that your custom action class extends PluginFlowStepActionDefinition and implements all of its required methods, and is registered using PluginFlowRegistrationService. The ChangeOrderStatusAction example demonstrates handling a list of orders as inputs and reporting per-order success or failure to the flow history.