Automations
On this page:
- Creating an automation mail
- Testing an automation mail
- Open & Click tracking
- Automation mail statistics
- Creating an automation
- Creating custom actions
- Creating custom triggers
- Creating conditions for the ConditionAction
- Creating custom placeholders
- Displaying webviews
- Custom Segmenting
- Troubleshooting
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.
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
: theunique_open_count
divided by thesent_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
: theunique_click_count
divided by thesent_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
: theunsubscribe_count
divided by thesent_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:
- Loop over each automation
- Loop over the actions in that automation
- 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.
PersonalizedReplacer
s 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
.