Siren

Custom Engagement Trigger Strategies

Implementing EngagementTriggerStrategy to define new ways collaborators earn attribution.

Last updated: April 6, 2026

Custom Engagement Trigger Strategies

Engagement triggers are the mechanism by which Siren attributes customer activity to collaborators. When a customer interacts with a collaborator’s referral link, uses a coupon code, or is manually attributed, an engagement trigger creates or updates engagement records that link that collaborator to the customer’s opportunity. These engagements are the foundation of Siren’s attribution system. They determine who gets credit when a conversion eventually occurs.

The Data Flow

The engagement trigger sits early in Siren’s pipeline:

Customer Action -> Event Fired -> Engagement Trigger Strategy -> Engagement Created -> (later) [Conversion](/documentation/resource-reference/conversions) -> [Obligation](/documentation/resource-reference/obligations) -> Payout

When a platform event fires (e.g., a site visit, a coupon being applied), the engagement trigger system checks all registered strategies to see which ones respond to that event. Each matching strategy inspects the event, locates the relevant collaborator, and creates or updates engagement records for every active program that collaborator participates in.

What interface do engagement triggers implement?

Every engagement trigger implements Siren\Engagements\Core\Interfaces\EngagementTriggerStrategy:

namespace Siren\Engagements\Core\Interfaces;

use PHPNomad\Events\Interfaces\Event;
use Siren\Engagements\Core\Models\Engagement;

interface EngagementTriggerStrategy
{
    /**
     * @return string The name of this strategy.
     */
    public function getName(): string;

    /**
     * @return string A description of the strategy.
     */
    public function getDescription(): string;

    /**
     * @return class-string<Event>[] A list of events that this trigger should fire against.
     */
    public function getTriggeringEvents(): array;

    /**
     * Attempts to create or update engagements in the context of the specified event.
     *
     * @param Event $event The event that triggered this strategy.
     * @return Engagement[] Array of created or updated engagement instances.
     */
    public function maybeCreateOrUpdateEngagements(Event $event): array;

    /**
     * Returns the unique identifier for this engagement trigger strategy type.
     *
     * @return string
     */
    public static function getId(): string;
}

getId() returns a unique string identifier (like 'referredSiteVisit' or 'boundCouponUsed') that gets stored in engagement records and used for registry lookups. getName() and getDescription() provide human-readable strings for the admin UI. getTriggeringEvents() returns the event classes this strategy responds to, which the system uses to know which strategies to invoke for a given event. The core logic lives in maybeCreateOrUpdateEngagements() — it receives the event, validates it, locates the collaborator, and creates or updates engagement records, returning an array of Engagement models (or an empty array if the strategy doesn’t apply).

Built-in Strategies

Siren ships with three engagement trigger strategies:

ReferredSiteVisit

Fires when a customer visits the site through a collaborator’s referral link. It listens for OpportunityTriggered events with trigger type 'site_visit' and locates the collaborator from the URL or request parameters. Strategy ID: 'referredSiteVisit'.

BoundCouponUsed

Fires when a customer applies a coupon code that is bound to a collaborator. It listens for CouponApplied events and locates the collaborator by looking up the coupon code as an alias. This strategy is only registered when the active commerce integration supports coupons. Strategy ID: 'boundCouponUsed'.

Manual

Fires when a manager manually attributes a transaction to a collaborator through the admin UI. It listens for ManualAttributionRequested events. Strategy ID: 'manual'. See Manually Attribute a Transaction for a user-level overview of when and how to use manual attribution.

Registration Pattern

Engagement triggers are registered by listening for the EngagementTriggerRegistryInitiated event and calling addStrategy() on the event object. The event uses the DI container to lazily instantiate strategies, so dependencies are auto-wired.

How Core Strategies Are Registered

In Siren\Engagements\Service\Initializer, the listener binding is declared:

public function getListeners(): array
{
    return [
        EngagementTriggerRegistryInitiated::class => RegisterCoreEngagementTriggerStrategies::class,
        // ...
    ];
}

The handler registers each strategy:

namespace Siren\Engagements\Core\Listeners;

use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use Siren\Engagements\Core\Events\EngagementTriggerRegistryInitiated;

class RegisterCoreEngagementTriggerStrategies implements CanHandle
{
    public function handle(Event $event): void
    {
        if ($event instanceof EngagementTriggerRegistryInitiated) {
            $event->addStrategy(ReferredSiteVisit::class);
            $event->addStrategy(Manual::class);
            // Conditionally add strategies based on feature support:
            // $event->addStrategy(BoundCouponUsed::class);
        }
    }
}

The addStrategy() method on the event registers a lazy factory in the registry:

public function addStrategy(string $strategyClass): void
{
    $this->registry->set($strategyClass::getId(), fn() => $this->provider->get($strategyClass));
}

Creating a Custom Engagement Trigger

Here is a complete example of a custom trigger that creates engagements when a student completes a course lesson:

Step 1: Define Your Event

If your triggering event does not already exist in Siren, create one:

namespace MyPlugin\Events;

use PHPNomad\Events\Interfaces\Event;
use Siren\Opportunities\Core\Models\Opportunity;

class LessonCompleted implements Event
{
    public function __construct(
        protected int $studentId,
        protected int $lessonId,
        protected Opportunity $opportunity
    ) {}

    public function getStudentId(): int { return $this->studentId; }
    public function getLessonId(): int { return $this->lessonId; }
    public function getOpportunity(): Opportunity { return $this->opportunity; }

    public static function getId(): string
    {
        return 'lesson_completed';
    }
}

Step 2: Implement the Strategy

namespace MyPlugin\Engagements;

use PHPNomad\Events\Interfaces\Event;
use PHPNomad\Utils\Helpers\Arr;
use Siren\Engagements\Core\Interfaces\EngagementTriggerStrategy;
use Siren\Engagements\Core\Services\CollaboratorActiveProgramService;
use Siren\Engagements\Core\Services\EngagementTriggerService;
use Siren\Programs\Core\Models\Program;
use MyPlugin\Events\LessonCompleted;
use MyPlugin\Services\CollaboratorFromStudent;

class LessonCompletionTrigger implements EngagementTriggerStrategy
{
    public function __construct(
        protected CollaboratorActiveProgramService $activeProgramService,
        protected EngagementTriggerService $engagementTriggerService,
        protected CollaboratorFromStudent $collaboratorFromStudent
    ) {}

    public function getName(): string
    {
        return 'Lesson Completed';
    }

    public function getDescription(): string
    {
        return 'Triggers an engagement when a referred student completes a lesson.';
    }

    public function getTriggeringEvents(): array
    {
        return [LessonCompleted::class];
    }

    public function maybeCreateOrUpdateEngagements(Event $event): array
    {
        if (!$event instanceof LessonCompleted) {
            return [];
        }

        // Locate the collaborator who referred this student
        $collaborator = $this->collaboratorFromStudent->locate($event->getStudentId());

        if (!$collaborator) {
            return [];
        }

        // Get all active programs this collaborator participates in
        // that support this engagement type
        $programs = $this->activeProgramService->getActiveProgramIds(
            $this->getId(),
            $collaborator->getId()
        );

        return Arr::map(
            $programs,
            fn(Program $program) => $this->engagementTriggerService->triggerEngagement(
                $this->getId(),
                $program,
                $collaborator,
                $event->getOpportunity()
            )
        );
    }

    public static function getId(): string
    {
        return 'lessonCompleted';
    }
}

Step 3: Register the Strategy

Create a listener and wire it in your Initializer:

namespace MyPlugin\Listeners;

use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use Siren\Engagements\Core\Events\EngagementTriggerRegistryInitiated;
use MyPlugin\Engagements\LessonCompletionTrigger;

class RegisterLessonCompletionTrigger implements CanHandle
{
    public function handle(Event $event): void
    {
        if ($event instanceof EngagementTriggerRegistryInitiated) {
            $event->addStrategy(LessonCompletionTrigger::class);
        }
    }
}

In your Initializer:

public function getListeners(): array
{
    return [
        EngagementTriggerRegistryInitiated::class => RegisterLessonCompletionTrigger::class,
    ];
}

Key Services

The EngagementTriggerService is the workhorse that custom strategies should delegate to. Its triggerEngagement() method handles:

  • Validating that the program, collaborator, and opportunity are all active
  • Finding or creating the engagement record
  • Calculating and accumulating the engagement score based on program configuration
  • Returning the resulting Engagement model

The CollaboratorActiveProgramService determines which programs a collaborator is enrolled in that support a given engagement type. Always use this to scope your trigger to the correct programs.

Removing or Replacing a Built-in Strategy

The registry event also supports deleteStrategy():

public function handle(Event $event): void
{
    if ($event instanceof EngagementTriggerRegistryInitiated) {
        // Remove a built-in strategy
        $event->deleteStrategy('referredSiteVisit');

        // Replace it with your own
        $event->addStrategy(MyCustomSiteVisit::class);
    }
}

This is useful when you need to change the behavior of a built-in trigger without creating a second, competing strategy.