You are currently reading the documentation for v7, while the latest version is v8.

Automations

On this page:

In this section you’ll learn how to programmatically work with automations.

Creating an automation mail

To send an email inside an automation, you must create an automation mail.

An automation mail can be created like this:

AutomationMail::create()
    ->from('sender@example.com')
    ->subject('Welcome to Mailcoach')
    ->content($html);

Alternatively, you could manually set the attributes on an AutomationMail model.

AutomationMail::create([
   'from_email' => 'sender@example.com',
   'subject' => 'My newsletter #1',
   'content' => $html,
]);

Setting the content and using placeholders

You can set the content of an automation mail by setting its HTML attribute.

$automationMail->html = $yourHtml;
$automationMail->save();

In that HTML you can use these placeholders which will be replaced when sending out the automation mail:

  • {{ unsubscribeUrl }}: this string will be replaced with the URL that, when hit, will immediately unsubscribe the person that clicked it
  • {{ unsubscribeTag['your tag'] }}: this string will be replaced with the URL that, when hit, will remove the “your tag” tag from the subscriber that clicked it
  • {{ webviewUrl }}: this string will be replaced with the URL that will display the content of your automation mail. Learn more about this in the docs on webviews.

If there is no way for a subscriber to unsubscribe, it will result in a lot of frustration on the part of the subscriber. We always recommend using {{ unsubscribeUrl }} in the HTML of each automation mail you send.

Setting a from name

To set a from name, just pass the name as a second parameter to from

AutomationMail::create()->from('sender@example.com', 'Sender name')

Setting a reply to

Optionally, you can set a reply to email and name like this

AutomationMail::create()->replyTo('john@example.com', 'John Doe')

Testing an automation mail

Before sending an automation mail, you can send a test to a given email address.

// to a single email address
$automationMail->sendTestMail('john@example.com');

// to multiple email addresses at once
$automationMail->sentTestMail(['john@example.com', 'paul@example.com'])

In the sent mail the placeholders won’t be replaced. Rest assured that when you send the mail to your subscribers, they will be replaced.

Open & Click tracking

The package can track when and how many times a subscriber opens or clicks an automation mail.

Enabling open & click tracking

To use this feature, you must configure your email provider to track opens.

How it works under the hood

Open tracking

When you send an automation mail, the email service provider will add a web beacon to it.. A web beacon is an extra img tag in the HTML of your mail. Its src attribute points to an unique endpoint on the domain of the email service provider.

Each time an email client tries to display the web beacon it will send a get request to email server. This way the email service provider will know the mail has been opened.

Via a web hook, the email service provider will let Mailcoach know that the mail has been opened.

Click tracking

When you send an automation mail that has click tracking enabled, the email service provider you use will replace each link in your mail by a unique link on its own domain. When somebody clicks that link, the email service provider will get a request, and it will know that the link was clicked. Next the request will be redirected to the original link.

Via a web hook, the email service provider will let Mailcoach know that a link has been clicked.

Automation mail statistics

After an automation mail is sent, some statistics will be made available.

On an automation mail

The scheduled ‘email-campaigns:calculate-statistics’ will fill these attributes on the AutomationMail model:

  • sent_to_number_of_subscribers
  • open_count: this is the total number of times your automation mail was opened. Multiple opens by a single subscriber will be counted.
  • unique_open_count: the number of subscribers that opened your automation mail.
  • open_rate: the unique_open_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, and the minimum is 0.
  • click_count: the total number of times the links in your automation mail were clicked. Multiple clicks on the same link by a subscriber will be counted.
  • unique_click_count: the number of subscribers who clicked any of the links in your automation mail.
  • click_rate: the unique_click_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, the minimum is 0.
  • unsubscribe_count: the number of people that unsubscribed from the email list using the unsubscribe link from this automation mail
  • unsubscribe_rate: the unsubscribe_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, the minimum is 0.

You can also get the opens and clicks stats for an automation mail. Here’s an example using the opens relation to retrieve who first opened the mail.

$open = $automationMail->opens->first();
$email = $open->subscriber->email;

On an automation mail link

If you enabled click tracking, an AutomationMailLink will have been created for each link in your automation mail.

It contains these two attributes that hold statistical data:

  • click_count: the total number of times this link was clicked. Multiple clicks on the same link by a subscriber will each be counted.
  • unique_click_count: the number of subscribers who clicked this link.

To know who clicked which link, you can use the relations on AutomationMailLink model. Here’s an example where we get the email of the subscriber who first clicked the first link of a mail.

$automationMailLink = $automationMail->links->first();
$automationMailClick = $automationMailLink->links->first();
$email = $automationMailClick->subscriber->email;

When are statistics calculated

The statistics are calculated by the scheduled mailcoach:calculate-automation-mail-statistics. This job will recalculate statistics:

  • each minute for campaigns that were sent between 0 and 5 minutes ago
  • every 10 minutes for campaigns that were send between 5 minutes and two hours ago
  • every hour for campaigns that were sent between two hours and a day
  • every four hours for campaigns that were sent between a day and two weeks ago

After two weeks, no further statistics are calculated.

Manually recalculate statistics

To manually trigger a recalculation, execute the command using the campaign id as a parameter.

php artisan mailcoach:calculate-automation-mail-statistics <automation-mail-id>

Creating an automation

An automation can be created like this:

Automation::create()
    ->name('Welcome email')
    ->to($emailList)
    ->runEvery(CarbonInterval::minute())
    ->triggerOn(new SubscribedTrigger)
    ->chain([
        new SendAutomationMailAction($automationMail),
    ])
    ->start();

The runEvery call is optional, accepts a CarbonInterval and will run every minute by default.

Setting the trigger

The trigger is what starts your automation, in most cases the \Spatie\Mailcoach\Domain\Automation\Support\Triggers\SubscribedTrigger will be used, this one triggers the automation once a subscriber is subscribed and confirmed.

Mailcoach ships with multiple triggers:

  • DateTrigger: Triggers on a date & time
  • NoTrigger: No trigger, which allows you to trigger the automation from code by calling $automation->run($subscriber) on a subscriber.
  • SubscribedTrigger: Triggers when a user is subscribed & confirmed
  • TagAddedTrigger: When a tag gets added to a subscriber
  • TagRemovedTrigger: When a tag gets removed from a subscriber
  • WebhookTrigger: Trigger the automation by calling a webhook

Mailcoach also allows you to create custom triggers.

Passing actions

The chain method accepts an array of automation actions that a subscriber will pass through once triggered.

Mailcoach ships with multiple actions:

  • SendAutomationMailAction: Send an automation mail
  • AddTagsAction: Add one or more tags to a subscriber
  • RemoveTagsAction: Remove one or more tags from a subscriber
  • ConditionAction: Branch the automation in true & false branches based on a condition, Mailcoach ships with the following conditions:
    • HasTagCondition: Whether the subscriber has a certain tag
    • HasClickedAutomationMail: Whether the subscriber has clicked one or any link in an automation mail
    • HasOpenedAutomationMail: Whether the subscriber has opened an automation mail
  • HaltAction: Stop the automation
  • UnsubscribeAction: Unsubscribe the subscriber from the list
  • WaitAction: Wait for a set interval before continuing to the next action

Mailcoach also allows you to create custom actions and custom conditions.

About Halt actions

The Halt action removes the subscriber from the automation when it reaches it, you usually put this at the end of your automation (or sometimes in an if/else block). Mailcoach works the following way:

  1. Loop over each automation
  2. Loop over the actions in that automation
  3. Loop over the subscribers that are attached to the action and run the action for it

If you don’t halt the automation for a subscriber that is at the end, it will keep doing those steps for that subscriber indefinitely.

This can be a useful feature for when you have a drip campaign that you want to attach more emails to in the future, but want to already start the campaign for subscribers, then the subscribers will move to the next action once that is added.

Creating custom actions

If you’re creating automations, you might run into a situation where you would like to have custom actions for your automation. Mailcoach allows you to extend the available actions easily.

Actions must extend the Spatie\Mailcoach\Domain\Automation\Support\Actions\AutomationAction class.

By default, the dropdown in the interface will show the classname of the action, you can implement the static method getName() to return a more user-friendly name for the action.

The getCategory method is required to implement and should be a value of the Spatie\Mailcoach\Domain\Automation\Support\Actions\Enums\ActionCategoryEnum enum.

There are three optional methods you can implement that control the flow of the automation once a subscriber reaches your action:

run(Subscriber $subscriber): void

This method is executed once, when the subscriber is added to this action for the first time, in the SendAutomationMailAction we use this to send the automation mail to the Subscriber:

public function run(Subscriber $subscriber): void
{
    $this->automationMail->send($subscriber);
}

shouldContinue(Subscriber $subscriber): bool

This method returns true by default, which means the Subscriber is moved on to the next action once the action’s run method has been called.

In the WaitAction, this is used to wait a certain duration before the subscriber is moved to the next action:

public function shouldContinue(Subscriber $subscriber): bool
{
    if ($subscriber->pivot->created_at <= now()->sub($this->interval)) {
        return true;
    }

    return false;
}

Inside actions, you have access to the pivot of the subscriber_actions relationship, which allows you to access when a subscriber was added to the action. The duration in the WaitAction is set in the UI, more on that below.

shouldHalt(Subscriber $subscriber): bool

This method returns false by default, this method allows you to completely halt the automation flow for a subscriber when returning true, even if there would be other actions after the current one.

Creating settings fields & validation

Most actions, like the WaitAction require some user configuration in the UI. When you need this there’s a few extra methods you can implement:

class WaitAction extends AutomationAction
{
    public CarbonInterval $interval;

    public function __construct(CarbonInterval $interval)
    {
        parent::__construct();

        $this->interval = $interval;
    }

    public static function getComponent(): ?string
    {
        return 'wait-action';
    }

    public static function make(array $data): self
    {
        return new self(CarbonInterval::createFromDateString("{$data['length']} {$data['unit']}"));
    }

    public function toArray(): array
    {
        [$length, $unit] = explode(' ', $this->interval->forHumans());

        return [
            'length' => $length,
            'unit' => Str::plural($unit),
        ];
    }
}

getComponent

The getComponent() method expects a Livewire component’s name to be returned. In this component, you can add any fields necessary for your trigger.

This component should extend our \Spatie\Mailcoach\Domain\Automation\Support\Livewire\AutomationActionComponent class, which allows you to have access to the current automation inside your component.

The getData method has to return the data you want stored inside the action.

For example, the wait-action component renders a simple blade view with a text field and has some validation rules:

The validation rules are stored on the Livewire component here, as the automation builder, which is also a Livewire component, handles all validation.

use Spatie\Mailcoach\Livewire\Automations\AutomationActionComponent;

class WaitActionComponent extends AutomationActionComponent
{
    public string $length = '1';

    public string $unit = 'days';

    public array $units = [
        'minutes' => 'Minute',
        'hours' => 'Hour',
        'days' => 'Day',
        'weeks' => 'Week',
        'weekdays' => 'Weekdays',
        'months' => 'Month',
    ];

    public function getData(): array
    {
        return [
            'length' => (int) $this->length,
            'unit' => $this->unit,
        ];
    }

    public function rules(): array
    {
        return [
            'length' => ['required', 'integer', 'min:1'],
            'unit' => ['required', Rule::in([
                'minutes',
                'hours',
                'days',
                'weeks',
                'weekdays',
                'months',
            ])],
        ];
    }

    public function render()
    {
        return view('mailcoach::app.automations.components.actions.waitAction');
    }
}

When creating an action component’s view, you should wrap this inside the <x-mailcoach::automation-action> blade component.

This will make sure the edit & save buttons are shown correctly. This is the view of the wait-action component as an example:

<x-mailcoach::automation-action :index="$index" :action="$action" :editing="$editing" :editable="$editable" :deletable="$deletable">
    <x-slot name="legend">
        {{__mc('Wait for ') }}
        <span class="form-legend-accent">
            {{ ($length && $unit && $interval = \Carbon\CarbonInterval::createFromDateString("{$length} {$unit}")) ? $interval->cascade()->forHumans() : '…' }}
        </span>
    </x-slot>

    <x-slot name="form">
        <div class="col-span-8 sm:col-span-4">
            <x-mailcoach::text-field
                :label="__mc('Length')"
                :required="true"
                name="length"
                wire:model="length"
                type="number"
            />
        </div>

        <div class="col-span-4 sm:col-span-2">
        <x-mailcoach::select-field
            :label="__mc('Unit')"
            :required="true"
            name="unit"
            wire:model="unit"
            :options="
                collect($units)
                    ->mapWithKeys(fn ($label, $value) => [$value => \Illuminate\Support\Str::plural($label, (int) $length)])
                    ->toArray()
            "
        />
        </div>
    </x-slot>
</x-mailcoach::automation-action>

make

The static make() method receives the validated data from the request, in this method you add the necessary parsing from raw data to your action’s required data structure and call the constructor.

toArray

The toArray() method is used to return the data in a format fit for processing in the Livewire component.

Registering your custom action

You can register your custom action by adding the classname to the mailcoach.automation.flows.actions config key.

Creating custom triggers

If you’re creating automations, you might run into a situation where you would like to have custom triggers for your automation.

There are two types of triggers: event based triggers and scheduled triggers, depending on your use case you can implement one of these.

Triggers must extend the Spatie\Mailcoach\Domain\Automation\Support\Triggers\AutomationTrigger class.

This class has a runAutomation method that accepts one or more Subscriber objects. This method will kickstart the automation for the subscriber(s).

By default, the dropdown in the interface will show the classname of the trigger, you can implement the static method getName() to return a more user-friendly name for the trigger.

Creating an event based trigger

Event based triggers start the automation, as the name suggests, when an event is triggered within your application. And must implement the Spatie\Mailcoach\Domain\Automation\Support\Triggers\TriggeredByEvents interface.

When creating an event based trigger, you’ll need to implement the subscribe method of the AutomationTrigger class, this class is an Event Subscriber, and gets registered automatically when attached to an automation.

We can look at the SubscribedTrigger as an example:

use Spatie\Mailcoach\Domain\Audience\Events\SubscribedEvent;

class SubscribedTrigger extends AutomationTrigger implements TriggeredByEvents
{
    public static function getName(): string
    {
        return (string) __mc('When a user subscribes');
    }

    public function subscribe($events): void
    {
        $events->listen(
            SubscribedEvent::class,
            function ($event) {
                $this->runAutomation($event->subscriber);
            }
        );
    }
}

As we can see here, the trigger will listen to the Spatie\Mailcoach\Domain\Campaign\Events\SubscribedEvent event and start the automation with the subscriber from that event.

Creating a scheduled trigger

Scheduled triggers are triggers that are ran by Laravel’s scheduler component. An example of a scheduled based trigger is the DateTrigger that Mailcoach ships with.

You must implement the Spatie\Mailcoach\Domain\Automation\Support\Triggers\TriggeredBySchedule interface when creating a scheduled trigger.

These triggers implement the trigger method, where you can run any code you need to determine if the trigger should fire for a certain amount of subscribers.

The date trigger checks if the current date & time is the same as the date & time that was set in the trigger (more on creating setting fields below), and fires the automation for all its subscribers once the date is equal.

use Spatie\Mailcoach\Domain\Automation\Models\Automation;

public function trigger(Automation $automation): void
{
    if (! now()->startOfMinute()->equalTo($this->date->startOfMinute())) {
        return;
    }

    $this->runAutomation($automation->newSubscribersQuery());
}

Creating setting fields & validation

Some triggers, like the DateTrigger require some user configuration in the UI. When you need this there’s a few extra methods you can implement:

class DateTrigger extends AutomationTrigger implements TriggeredBySchedule
{
    public CarbonInterface $date;
    
    public function __construct(CarbonInterface $date)
    {
        parent::__construct();
    
        $this->date = $date;
    }
        
    public static function getComponent(): ?string
    {
        return 'date-trigger';
    }
    
    public static function make(array $data): self
    {
        return new self((new DateTimeFieldRule())->parseDateTime($data['date']));
    }
    
    public static function rules(): array
    {
        return [
            'date' => ['required', new DateTimeFieldRule()],
        ];
    }
}

getComponent

The getComponent() method expects a Livewire component’s name to be returned. In this component, you can add any fields necessary for your trigger.

This component should extend our \Spatie\Mailcoach\Domain\Automation\Support\Livewire\AutomationTriggerComponent class, which allows you to have access to the current automation inside your component.

For example, the date-trigger component renders a simple blade view with a date & time field:

use Spatie\Mailcoach\Livewire\Automations\AutomationTriggerComponent;

class DateTriggerComponent extends AutomationTriggerComponent
{
    public function render()
    {
        public function render()
    {
        return view('mailcoach::app.automations.components.triggers.dateTrigger');
    }
    }
}

And the view:

<div>
    <x-mailcoach::date-time-field
        :label="__mc('Date')"
        name="date"
        :value="$automation->trigger->date ?? null"
        required
    />
</div>

make

The static make() method receives the validated data from the request, in this method you add the necessary parsing from raw data to your component’s required data structure and call the constructor.

rules

The rules method on the Trigger class (not the Livewire component) allows you to specify rules for the fields you’ve created in the Livewire component.

Registering your custom trigger

You can register your custom trigger by adding the classname to the mailcoach.automation.flows.triggers config key.

Creating conditions for the ConditionAction

Mailcoach ships with a ConditionAction that allows you to define a condition and split the automation in a true and false branch.

By default this action ships with 3 conditions:

  • Subscriber has a specific tag
  • Subscriber has opened an automation mail
  • Subscriber has clicked (one or all) links in an automation mail

You can also create your own conditions by creating a class that implements the Spatie\Mailcoach\Domain\Automation\Support\Conditions\Condition interface.

Let’s take a look at the HasTagCondition class as an example:

namespace Spatie\Mailcoach\Domain\Automation\Support\Conditions;

use Spatie\Mailcoach\Domain\Audience\Models\Subscriber;
use Spatie\Mailcoach\Domain\Automation\Models\Automation;

class HasTagCondition implements Condition
{
    public function __construct(
        private Automation $automation,
        private Subscriber $subscriber,
        private array $data,
    ) {
    }

    public static function getName(): string
    {
        return (string) __mc('Has tag');
    }

    public static function getDescription(array $data): string
    {
        return (string) __mc(':tag', ['tag' => $data['tag']]);
    }

    public static function rules(): array
    {
        return [
            'tag' => 'required',
        ];
    }

    public function check(): bool
    {
        return $this->subscriber->hasTag($this->data['tag']);
    }
}

__construct

The construct method receives the current automation, the subscriber you’re checking on and an array of data.

The array of data is only used by the default conditions, custom conditions will always receive an empty array.

getName & getDescription

The static getName() method gets called to show the name of the condition in the dropdown. The getDescription() method gets shown on the summary of the action.

check

The check() method is the heart of your condition. Anything is possible in this method, in the example above we’re simply checking if the subscriber has a specific tag.

Creating custom placeholders

By default, Mailcoach offers a couple of placeholders you can use in the subject or content of your automation mail, such as webviewUrl and unsubscribeUrl.

Creating a normal replacer

Custom placeholders can be created. Do this you must create a class and let it implement Spatie\Mailcoach\Domain\Automation\Support\Replacers\AutomationMailReplacer interface.

This interface contains two methods. In replace you must do the actual replacement. In helpText you must return the helptext that will be visible on the automation mail content screen.

Here is the code of the WebviewAutomationMailReplacer that ships with Mailcoach.

namespace Spatie\Mailcoach\Domain\Automation\Support\Replacers;

use Spatie\Mailcoach\Domain\Automation\Models\AutomationMail;

class WebviewAutomationMailReplacer implements AutomationMailReplacer
{
    public function helpText(): array
    {
        return [
            'webviewUrl' => __mc('This URL will display the HTML of the automation mail'),
        ];
    }

    public function replace(string $text, AutomationMail $automationMail): string
    {
        $webviewUrl = $automationMail->webviewUrl();

        return str_ireplace('::webviewUrl::', $webviewUrl, $text);
    }
}

After creating a replacer you must register it in the automations.replacers config key of the mailcoach config file.

Creating a personalized replacer

A regular replacer will do its job when the automation mail is being prepared. This will only happen once when sending an automation mail. There’s also a second kind of replacer: Spatie\Mailcoach\Domain\Automation\Support\Replacers\PersonalizedReplacer. These replacers will get executed for each mail that is being sent out in an automation mail.
PersonalizedReplacers have access to subscriber they are sent to via the Send object given in the replace method.

Here is the code of the UnsubscribeUrlReplacer that ships with Mailcoach.

namespace Spatie\Mailcoach\Domain\Automation\Support\Replacers;

use Spatie\Mailcoach\Domain\Shared\Models\Send;

class UnsubscribeUrlReplacer implements PersonalizedReplacer
{
    public function helpText(): array
    {
        return [
            'unsubscribeUrl' => __mc('The URL where users can unsubscribe'),
            'unsubscribeTag::your tag' => __mc('The URL where users can be removed from a specific tag'),
        ];
    }

    public function replace(string $text, Send $pendingSend): string
    {
        $unsubscribeUrl = $pendingSend->subscriber->unsubscribeUrl($pendingSend);

        $text = str_ireplace('::unsubscribeUrl::', $unsubscribeUrl, $text);

        preg_match_all('/::unsubscribeTag::(.*)::/', $text, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            [$key, $tag] = $match;

            $unsubscribeTagUrl = $pendingSend->subscriber->unsubscribeTagUrl($tag);

            $text = str_ireplace($key, $unsubscribeTagUrl, $text);
        }

        return $text;
    }
}

Displaying webviews

Whenever you send an automation mail, a webview is also created. A webview is a hard to guess URL that people who didn’t subscribe can visit to read the content of your mail.

You can get to the URL of the webview of an automation mail:

$automationMail->webViewUrl();

Customizing the webview

You can customize the webview. To do this, you must publish all views:

php artisan vendor:publish --provider="Spatie\Mailcoach\MailcoachServiceProvider" --tag="mailcoach-views"

After that, you can customize the webview.blade.php view in the resources/views/vendor/mailcoach/automation directory.

Custom Segmenting

If you wish to send an automation to only a part of an email list you can use a segment when creating your automation. A segment is a class that is responsible for selecting subscribers on an email list. It should always extend Spatie\Mailcoach\Domain\Audience\Support\Segments\Segment

A first example

Here’s a silly segment that will only select subscribers whose email address begin with an ‘a’

class OnlyEmailAddressesStartingWithA extends Segment
{
    public function shouldSend(Subscriber $subscriber): bool
    {
        return Str::startsWith($subscriber->email, 'a');
    }
}

When create an automation this is how the segment can be used:

Automation::create()
   ->segment(OnlyEmailAddressesStartingWithA::class);

Using an instantiated Segment object

Here’s the same segment that will only select subscribers whose email address begin with a configurable character ‘b’

class OnlyEmailAddressesStartingWith extends Segment
{
    public string $character;

    public function __construct(string $character) {
        $this->character = $character;
    }

    public function shouldSend(Subscriber $subscriber): bool
    {
        return Str::startsWith($subscriber->email, $this->character);
    }
}

When sending a campaign this is how the segment can be used:

Automation::create()
    ->segment(new OnlyEmailAddressesStartingWith('b'));

The object will be serialized when saved to the automation, and unserialized when used for segmenting.

Using a query

If you have a very large list, it might be better to use a query to select the subscribers of your segment. This can be done with the subscribersQuery method on a segment.

Here’s an example:

class OnlyEmailAddressesStartingWithA extends Segment
{
    public function subscribersQuery(Builder $subscribersQuery): void
    {
        $subscribersQuery->where('email','like', 'a%');
    }
}

No matter what you do in subscribersQuery, the package will never mail people that haven’t subscribed to the email list you’re sending the automation to.

Segment description

Spatie\Mailcoach\Domain\Audience\Support\Segments\Segment allows us to give our custom segment a unique name. This is required by the interface and can be done very easily:

public function description(): string
{
    return 'My cool segment';
}

Accessing the Automation model

If you need to get any automation details somewhere in your segment logic, you can use $this->segmentable to access the model object of the automation that is being sent.

Troubleshooting

When sending an automation mail, the package will create Send models for each mail to be sent. A Send has a property sent_at that stores the date time of when an email was actually sent. If that attribute is null the email has not yet been sent.

If you experience problems while sending, and the state of your queues has been lost, you should dispatch a SendAutomationMailJob for each Send that has sent_at set to null and the automation_mail_id is not null.

Send::query()
    ->whereNull('sent_at')
    ->whereNotNull('automation_mail_id')
    ->each(function(Send $send) {
       dispatch(new SendAutomationMailJob($automationMailSend);
    });

You can run the above code by executing this command:

php artisan mailcoach:retry-pending-automation-mail-sends

Should, for any reason, two jobs for the same Send be scheduled, it is highly likely that only one mail will be sent. After a SendAutomationMailJob has sent an email it will update sent_at with the current timestamp. The job will not send a mail for a Send whose sent_at is not set to null.

Audience
Campaigns