Workflow extensions
Manifest and runtime contracts for plugin workflow handlers, triggers, and conditions.
Extension contract
Workflow extensions are declared in signals.yaml and activated only when the
plugin manifest is valid, compatible with the host, and enabled. The supported
manifest sections are:
workflow_handlersfor executable action steps;workflow_triggersfor plugin-owned trigger events and their input/output schemas; andworkflow_conditionsfor constrained, side-effect-free decision points.
Every declaration has a vendor-namespaced identifier, positive version and
contract_version, a class, capabilities, and typed schemas. The handler
declaration also describes outcomes, sensitive fields, and retry safety. A
plugin may register its handler through PluginRegistrar::workflowHandler();
the registrar requires the complete manifest declaration for a scoped plugin.
See the manifest reference for field validation and registries and resolvers for runtime registration.
Handlers
A handler is resolved from the container by its persisted identifier. Its declared capabilities are checked through the workflow service principal before execution. Identifiers are global and collisions are rejected; a plugin cannot replace a core handler or another plugin's handler by reusing its key.
Automatic retry is allowed only when the declaration marks the handler idempotent and safe for automatic retry. The handler's declared outcomes and schemas are used during workflow publication and authoring. The execution runtime records attempts, outputs, failures, and redacted sensitive fields as workflow evidence.
Triggers
Plugin triggers are manifest-driven extensions to the trigger catalogue. Their declared input and output schemas, capabilities, and sensitive fields describe the boundary presented to a workflow. The trigger registry matches an incoming event against the registered extension and passes the constrained workflow trigger context; it does not discover arbitrary classes at runtime.
Conditions
Plugin conditions are deliberately narrower than handlers. A condition must be
declared sanitized and side_effect_free, and its context paths must be
limited to the allowlisted workflow context (trigger, subject,
originator, workflow, steps, and vars). Manifest validation rejects
database/model queries and mutations, including query, write, update, delete,
or database capabilities.
The condition registry evaluates a persisted plugin reference containing the plugin and plugin version, condition identifier and version, and its arguments. Conditions receive the sanitized context and return a decision; they do not perform live queries or mutate application state.
Health, compatibility, and lifecycle
Host-version requirements, dependencies, conflicts, extension identity, and
contract versions are validated before activation. A workflow dependency can be
reported as available, incompatible, disabled, missing,
upgrade_required, or invalid. Dependency health blocks new runs and the
affected publication/activation paths, while pinned active-run behavior and
redacted diagnostics remain available for investigation.
Plugin workflow registrations are activated atomically. Replacing a manifest registers the new handler, trigger, and condition declarations as one operation; a failed replacement restores the previous registrations. Disabling or removing a plugin removes its owned registrations and leaves persisted workflow references visible as unhealthy rather than silently rebinding them.
Enable or install a compatible plugin, or publish a replacement workflow version, before starting new runs that depend on an unhealthy extension. There is no arbitrary plugin discovery, execution shadow mode, or live condition query path in this contract.
Domain-event capabilities
PluginRegistrar::event() accepts the existing visibility array followed by optional named arguments description, subject, redaction, samplePayload, provider, and contract_version. webhookEvent() accepts the same metadata and a trailing workflow: true option when the event is declared workflow-visible. Both methods forward the metadata into EventDefinition; omitted metadata is read from the matching manifest event.
$registrar->event(
'acme.sync.completed',
['workflow' => true, 'webhook' => true],
description: 'An account sync completed.',
subject: ['type' => 'account', 'id_path' => 'subject.id'],
redaction: ['sensitive_fields' => ['email']],
samplePayload: ['subject' => ['id' => 1]],
provider: 'acme.sync',
contract_version: 1,
);
A manifest events[] entry carries those same metadata fields and may declare provider_class, the installed class implementing WorkflowEventCapabilityProvider. Its providerKey() and contractVersion() must match provider and contract_version. The workflow extension manager validates and owns these class descriptors, removes them on deactivation, restores them on replacement compensation, and constructs fresh providers for each runtime scope. Keep providers stateless; obtain the active tenant connection when resolving or authorizing a subject. Framework service providers may also register scoped capability classes with the WorkflowEventCapabilityProvider::class container tag.
Complete event metadata still leaves an event catalogue-only unless a compatible provider is registered. A provider must resolve and authorize the actual subject and adapt/redact the event payload; it must not infer authority from a catalogue visibility flag. Even a triggerable plugin domain fact cannot be emitted by Fire Event.
Event batches remain atomic: EventRegistry::registerMany() validates every member before mutating the catalogue. Plugin boot checkpoints restore event definitions and provider descriptors together when registration fails. Nonterminal runs retain event-specific definitions and provider class names through the existing workflow retention store; preserve old classes while they are pinned. A retained definition is never reinserted into active discovery to enable new runs.
Provider versions, retained contracts, and binder access
Published domain-event triggers pin provider, contract_version, and
payload_version. Admission checks all three against the current event and
provider; a mismatch refuses a new run. A manifest's provider_class must
implement WorkflowEventCapabilityProvider, and its providerKey() and
contractVersion() must match the event declaration. Version incompatible
contracts explicitly rather than changing a class still needed by a pinned run.
WorkflowExtensionRetentionService retains validated handler and condition
metadata plus event-specific definitions and provider class names for
nonterminal runs. Existing runs can resolve the retained provider against its
retained event definition and reauthorize the subject. The old PHP classes
must remain installed: retention stores metadata, not executable code. Retained
entries are separate from active discovery and do not admit new runs. Metadata
is eligible for collection only after no nonterminal run pins it.
There is no general plugin opt-in to a shared outbound binder in the current
runtime. Handler execution receives WorkflowHandlerContext, including the
configuration, execution data, subject and service principal; do not treat it
as an automatically redacted projection. Plugin conditions instead receive
WorkflowConditionContext with only declared context paths, and both condition
evaluation paths omit paths overlapping declared redactions. Conditions cannot
request query or mutation capabilities. For communication templates, the
existing renderer binds only paths in its variable catalogue and refuses an
unlisted path; this does not grant a plugin arbitrary model, container or
unredacted-context access through a template. Keep outbound data within the
specific handler's declared contract and redaction rules.