How do I correctly delay the execution of a campaign action in a plugin?

Hello, I’m developing a plugin for Mautic 5.2 which adds an action to the campaign editor which delays the execution until a later moment. But when I try to reschedule the action by failing the event, Mautic disables the campaign due to multiple failings.

How would I correctly delay the execution of a campaign action in a plugin?

Currently, I’ve got the following code:

 public function onExecuteCampaignAction(PendingEvent $event): void
    {
        foreach ($event->getPending() as $pending) {
            
            $delayUntil = new \DateTime($this->getTheDelayFromAService());

            if ($delayUntil > new \DateTime('now')) {
                // Schedule for later execution
                $event->fail($pending, 'Delay date not reached yet', $delayUntil->diff(new \DateTime('now')));
            } else {
                // Pass the event
                $event->pass($pending);
            }
        }
    }

Searching further, I tried the following code without success (The method passWithError() resets the logs isScheduled and triggerDate fields):

if ($delayUntil > new \DateTime()) {
      // If the delay date is in the future, schedule it
      $log->setTriggerDate($delayUntil);
      $log->setIsScheduled(true);
      // Pass with note about scheduling
      $event->passWithError(
            $log,
            'Dynamic delay until ' . $delayUntil->format('Y-m-d H:i:s')
       );
} else {
    // Pass the event immediately if the date is now or in the past
    $event->pass($log);
}

Apparently more than 10% failed event disables a campaign automatically. The code responsible is in the campaign bundle, class CampaignEventSubscriber#onEventFailed.

So failing an event is the wrong way. But passing an event clears the rescheduling. I’m at a loss on how to do this.

Is something like this not working? mautic/app/bundles/CampaignBundle/EventListener/CampaignActionJumpToEventSubscriber.php at b6e6692fcb4d47c41915f3a4a12119a38d034229 · mautic/mautic · GitHub

Using the event scheduler might work, I’ll try.

But what about the case when in one PendingEvent there are multiple LeadEventLog entries with different delays?

public function onExecuteCampaignAction(PendingEvent $event): void
    {
        foreach ($event->getPending() as $pending) {
            
            // Get delay for this log, might be a different lead
            $delayUntil = new \DateTime($this->getTheDelayFromAServiceDependingOnTheLead($pending));

            if ($delayUntil > new \DateTime('now')) {
                // Schedule for later execution
                $event->fail($pending, 'Delay date not reached yet', $delayUntil->diff(new \DateTime('now')));
            } else {
                // Pass the event
                $event->pass($pending);
            }
        }
    }

I guess I don’t really understand the relationship between LeadEventLog entries and the event itself.

There are 2 event classes here. One is the event used in the dispatcher, the PendingEvent. Just a DTO object containing some information about campaign execution that is happening. It contains a campaign Event which is a Doctrine entity class which is a representation of the campaign_events table record. It has an information about what should be done for the contacts passing through the campaign. And there is also a collection of the LeacEventLog records which are records from the campaign_lead_event_log table that are representation of what should/was done to specific contact passing through the campaign event.

I’m not sure I made it clearer or the other way around…

Thank you for your explanations.

My assumption was that every contact going through a campaign gets its own event. This is obviously not the case.

I think what I’m trying to do here is not supported by the Mautic architecture and I will have to do a workaround.

(For context: I am trying to design a campaign which gets immediately triggered by orders in a ticket shop, but should send out a reminder mail only the day before the ticket is valid. That’s why I was writing a dynamic delay action which was supposed to wait until a date specified in a form field.)

I think this could be done without coding:

  1. Create a segment “Created Order” where you add contacts that make an order. You didn’t mention how that happen but I guess some custom field is set so you can filter for it.
  2. Create a campaign with the source of the “Created order” segment that will send the email with the ticket confirmation.
  3. Create a segment “Ticket Valid Reminder” which will filter for contacts that have the “event valid date” custom field set for “today”
  4. Create a campaign that will use the “Ticket Valid Reminder” contacts and send the reminder email.

Of course this is relative to whether you can have the custom field with the date. If you expect the contacts to have more custom fields assigned to the contact then you may try GitHub - acquia/mc-cs-plugin-custom-objects: Mautic plugin adding Custom Objects feature

Thank you very much. I think I’ll go with your segments solution instead of further fiddling with events.

My contacts are created with a custom integrations plugin. Ticket orders are currently simulated form submissions so I can attach them to a contact. I’ll look into the custom objects plugin for storing the orders, that sounds promising as well.

Thanks again for your help.

Could they have several tickets? Then you probably have no other choice than custom objects.

Yes, that could be the case.

Currently installing the custom objects plugin. :wink: