SIGNALS Documentation
API Reference

Plugin events

EventRegistry for plugins — visibility flags, registrar event()/webhookEvent(), finding hookable names, and core event families.

What it is

App\Services\Events\EventRegistry is the canonical domain-event catalogue. Plugin Event and Filter hooks must use names that exist in this registry (enforced when $registrar->hook() runs for those types).

Contract page: Event Registry.

Seed source: config/events.php (hundreds of named events). Enumerate at runtime:

app(\App\Services\Events\EventRegistry::class)->names();
// or
array_keys(config('events.events'));

Visibility flags

Each EventDefinition carries consumer flags:

Flag Meaning
api Visible to API / activity consumers that opt into API visibility
webhook Eligible for outbound webhook subscriptions
workflow Eligible for workflow triggers
plugin Eligible for plugin discovery / plugin consumers
system_protected Excluded from names() / describe() listings; still present for has() / get()

Consumers query via visibleTo('api'|'webhook'|'workflow'|'plugin').

Declaring plugin-owned events (events[])

Plugin-provided catalogue names must appear in signals.yaml events[] (wiring ⊆ declaration). Names are vendor-prefixed like permissions (e.g. package acme/example → acme.sync.completed). Rules: events.malformed, events.namespace.

# signals.yaml
events:
  - name: acme.sync.completed
    label: Sync Completed
    webhook: true          # required for webhookEvent() / event(..., webhook: true)

hooks:
  # Other plugins (or this one) that want to *handle* the event declare it here.
  - name: acme.sync.completed
    type: event
    priority: 50

Registering plugin-owned events

From PluginRegistrar (signatures copied from source):

public function webhookEvent(string $name): self

public function event(string $name, array $visibility = []): self
// accepted visibility keys: api?, webhook?, workflow?, plugin?, payload_schema?
  • webhookEvent($name) registers with EventSource::Plugin, webhook: true, and adds the name to WebhookEventRegistry so it is subscribable in the webhooks UI/API.
  • event($name, $visibility) registers a catalogue entry with the supplied flags; webhook: true also registers into WebhookEventRegistry.

Outbound delivery is automatic. A declared event with webhook: true is delivered to subscribers by either dispatch route:

  • Signals::emit($package, $name, $payload) — the SDK path, which also runs plugin Event hooks.
  • Event::dispatch($name, [$payload]) — a plain Laravel event, bridged by App\Listeners\DispatchPluginWebhookEvent, which subscribes to every plugin-source catalogue entry with webhook visibility at boot.

Neither route needs plugin-specific wiring, and neither re-fires while the Verbs event store is replaying. Pass a single associative array as the payload; other shapes are wrapped under a payload key.

Manifest gate: both methods require name ∈ events[]. When registering webhook visibility, the manifest entry must also have webhook: true. (hooks[] still gates hook() / container listeners — it is the handler subscription list, not the provided-event list.)

Typical order:

$registrar->event('acme.sync.completed', [
    'plugin' => true,
    'webhook' => true,
]);

// A different plugin (or the same one) may then:
$registrar->hook('acme.sync.completed', HookType::Event, $handler, 50);

Firing plugin events — Signals::emit()

Signals::emit('acme/example', 'acme.sync.completed', ['job_id' => 42]);

// From a hook handler:
$context->emit('acme.sync.completed', ['job_id' => 42]);

Fire path: PluginHookDispatcher::dispatchEvent($name, $payload) — the same sink the AuditableEvent / Laravel string-event bridges use — then, when the catalogue entry has webhook: true, WebhookService::dispatch($name, $payload).

Guards: plugin enabled; name declared in that package's events[]; name registered in EventRegistry.

Subscribing to CORE events — listen()

$registrar->listen($eventName, $listener, $priority = 50) subscribes to any catalogued domain event by its published name — never a class path — so plugins speak the same stable vocabulary webhook subscribers do:

$registrar->listen('rental.created', function (PluginContext $context, PluginEventPayload $event): void {
    $context->log('New rental '.$event->get('id').' by '.$event->actorType());
});

// A container-resolved invokable class is also accepted:
$registrar->listen('invoice.issued', AcmeInvoiceListener::class);

The listener receives the usual PluginContext first, then an immutable App\Sdk\Hooks\PluginEventPayload carrying the published payload plus the actor_type / ref stamps ($event->event, ->get($key), ->actorType(), ->ref()).

Rails, all enforced by App\Sdk\Hooks\PluginEventDispatcher rather than by convention:

Rail Behaviour
Catalogue gate Only names with plugin: true and NOT system_protected may be subscribed
Fail loud An unknown or withheld name throws PluginRegistrarException at plugin boot, not silently at runtime
Replay guard Listeners never fire while Verbs::isReplaying() — same contract as outbound webhooks
Failure isolation A throwing listener is logged, counted against its package, and auto-disables the plugin at plugins.hook_failure_threshold; it can never break the business action

Manifest gate: the event name must appear in hooks[] (it is a handler subscription, not a provided event). Subscriptions live in the shared PluginHookRegistry under the namespaced hook name event:<name>, so they can never be fired by — or collide with — a hand-placed hook() of the same name.

Fan-out happens in WebhookService::dispatch(), the one boundary every catalogue-named emitter already funnels through: plugin and webhook subscribers see the same event, exactly once per domain action, with no per-event wiring.

Finding hookable names

  1. Prefer names already in config/events.php that match the domain moment you care about (e.g. account.updated, invoice.issued).
  2. Confirm with EventRegistry::has($name).
  3. For integrations, rely on the AuditableEvent bridge (see Plugin hooks) — CRM/invoice actions fire AuditableEvent with $action equal to the catalogue name.
  4. Register new names only when you own the domain moment and will dispatch them (or expose them for webhooks).

OpenAPI / REST activity is separate from this catalogue; webhook delivery uses Webhook Event Registry.

Core families (from config/events.php)

Prefixes present today (non-exhaustive counts from the seed file):

Prefix Approx. count Examples
rental.* 67 rental lifecycle, items, quotes
account.* 22 account.updated, account CRM events
flightcase.* 14 pack / seal / dissolve / dispatch
shortage.* 13 shortage detection / resolution
stock_check.* 11 stocktake lifecycle
document.* 9 document generate / finalise
plugin.* 8 see below
catalogue_item.* 7 catalogue changes
invoice.* 4 includes invoice.issued

Also present under other prefixes: import/export-related names (when seeded), stock, purchase orders, notifications, etc. Always check the live catalogue rather than hard-coding an incomplete list.

Built-in plugin.* catalogue names

  • plugin.enabled
  • plugin.disabled
  • plugin.installed
  • plugin.uninstalled
  • plugin.updated
  • plugin.settings_updated
  • plugin.data_access_denied
  • plugin.data_updated

Self-subscription

A plugin may hook its own provided event — declare the name in both events[] (provider side) and hooks[] (consumer side), call event() before the hook() that subscribes to it (Event hook names must already exist in EventRegistry), and never re-emit from the handler (that would loop). signals/slots-demo does exactly this: SlotsDemoListener emits signals.slotsdemo.logged after logging an account update and SlotsDemoEchoListener receives it — see Plugin examples.