SIGNALS Documentation
API Reference

Workflow Handler Registry

Contract for registering workflow action handlers under their persisted identifiers and resolving them through the container.

Overview

App\Services\Workflows\WorkflowHandlerRegistry maps the action identifiers persisted on workflow steps to App\Contracts\Workflows\WorkflowHandler implementations.

WorkflowServiceProvider binds the registry as a singleton constructed with the application container. Core registers its own handlers on boot; plugins register additional handlers through PluginRegistrar::workflowHandler().

Identity is deliberately absent from the handler contract itself. A handler describes and executes an action; the identifier under which that action is persisted belongs to the registry, so the same handler class can be registered under a different identifier without editing it.

Public surface

Method Purpose
register() Warehouse one handler class under a new identifier
get() Resolve one handler by identifier or throw
has() Check whether an identifier is registered
all() Resolve every registered handler, keyed by identifier

Accepted contract

The registered value is a class-string for a WorkflowHandler implementation, which requires:

  • name(): string — human-readable name shown when building a workflow
  • description(): string — short explanation of the action's behaviour
  • group(): string — logical group used by workflow-builder presentation
  • configSchema(): Schema — declarative configuration schema for the action
  • outputs(): array — possible outputs keyed by paths relative to the action's result
  • execute(WorkflowHandlerContext $context): WorkflowHandlerResult — run the action

Handler failures are represented by exceptions rather than by a failure result value, so the execution engine can apply its own failure policy.

Identity, collisions, and construction

register() validates eagerly and refuses to overwrite. It throws InvalidArgumentException for a blank identifier, for a class that does not exist or does not implement WorkflowHandler, and for a class that is not instantiable; it throws LogicException when the identifier is already registered.

Rejecting duplicates rather than replacing them is the important property: identifiers are persisted on saved workflows, so a silent replacement would change what an existing workflow step does at its next execution.

The registry stores class strings, not handler objects. get() and all() resolve through the container on every call, so handler dependencies stay injectable and test doubles can replace concrete classes through the container. A resolved object that does not implement WorkflowHandler raises LogicException.

Failure behaviour

get($identifier) throws InvalidArgumentException with Unknown workflow handler [{identifier}]. when nothing is registered under that identifier. all() shares the resolution path, so a definition whose class cannot be built surfaces the container's own failure.

Plugin registration

PluginRegistrar::workflowHandler($identifier, $handlerClass) is the plugin-facing entry point. Handler identifiers are global and persisted, so a plugin-scoped registrar requires the identifier to carry the plugin's vendor prefix — the same rule the manifest validator applies to permission, event, and datatable keys. Core identifiers are unprefixed, which is what prevents a plugin from shadowing or replacing a core handler.

Registry validation failures surface from the registrar as PluginRegistrarException naming the package responsible, with the registry's own exception as the previous exception. Unscoped registrars — core and tests — are not prefix-gated.

Worked example

Registering a handler and executing it by its persisted identifier:

$registry = app(WorkflowHandlerRegistry::class);
$registry->register('send_email', SendEmailHandler::class);

if ($registry->has($step->action)) {
    $result = $registry->get($step->action)->execute($context);
}

all() returns the same resolved objects keyed by identifier, which is what a workflow builder consumes when it lists the available actions with their names, groups, config schemas, and outputs.