Inherent.Automation
Email automation plugin for OctoberCMS (from v3 onward). Manages subscriber lists, double opt-in, multi-step email sequences, open/click tracking, and unsubscribe (RFC 8058 compliant).
Replaces paid email marketing automation systems like system.io, mailerlite, brevo, etc...
Integration with RainLab.User, Responsiv.Campaign, MelonCart, Mall
Installation via Command Line
php artisan plugin:install Inherent.Automation
Inherent.Automation
Email automation plugin for OctoberCMS (from v3 onward). Manages subscriber lists, double opt-in, multi-step email sequences, open/click tracking, and unsubscribe (RFC 8058 compliant).
Replace email automation like system.io, mailerlite, brevo, etc...
Requirements
- OctoberCMS v3
- PHP 8.2+
- A configured mail driver (SMTP, Mailgun, SES…)
Installation
- After installation from the marketplace :
Add the scheduler entry to your crontab. Use the full PHP binary path — cron does not inherit your shell
PATH:-ethen add the line* * * * * "/path/to/your/root/project/php" /path/to/artisan schedule:run >> /dev/null 2>&1
The scheduler runs automation:process every minute to dispatch queued emails.
Backend user guide
Subscriber lists
Automation > Lists
A list groups subscribers under a name and optional sender identity. Each list has a unique code (auto-generated from the name, editable).
Mark one list as default so the subscribe form falls back to it when no explicit list is configured.
| Field | Description |
|---|---|
| Name | Display name |
| Code | Unique slug, used in templates and API |
| From name | Sender name for emails sent to subscribers of this list |
| From email | Sender address. Falls back to mail.from if empty |
| Is default | Used by the frontend component when no list is specified |
Subscribers
Automation > Subscribers
A subscriber is identified by email address. The subscriber record carries a global status that acts as a high-water mark:
| Status | Meaning |
|---|---|
pending |
Signed up, confirmation email sent, not yet confirmed on any list |
confirmed |
Confirmed on at least one list |
unsubscribed |
Opted out |
bounced |
Set manually for hard bounces |
Subscription status is tracked per list, not globally. The same subscriber can be confirmed on list A and pending on list B — both the global status and the per-list pivot status coexist independently. The Lists tab on the subscriber form shows the pivot status, subscription date, and unsubscription date for each list.
Adding a list from the backend (via the Add button) confirms the subscription immediately, bypassing double opt-in.
The backend form page shows all list memberships and the full email history for that subscriber.
Email templates
Automation > Email Templates
Reusable email bodies written in HTML + Twig. Templates define subject, HTML body, and optional sender overrides (from name, from email).
Available Twig variables:
| Variable | Value |
|---|---|
{{ subscriber.email }} |
Subscriber email address |
{{ subscriber.first_name }} |
First name |
{{ subscriber.last_name }} |
Last name |
{{ subscriber.full_name }} |
First + last name |
{{ unsubscribe_url }} |
Unique unsubscribe link (per-email tracking code) |
{{ site_url }} |
Site root URL |
{{ site_name }} |
App name from config |
{{ subject }} |
Email subject |
The layout file (views/mail/email_layout.htm) wraps all rendered bodies. Edit it to change the global email frame. It receives the same variables plus {{ body }}.
Automations
Automation > Automations
An automation is a sequence of steps triggered by an event.
| Field | Description |
|---|---|
| Name | Internal label |
| Status | draft (never runs), active (processes queue), inactive (paused) |
| Trigger type | See below |
| Trigger lists | Visible for subscribe_to_list: which lists trigger this automation |
| Event name | Visible for custom_event: a code developers pass to AutomationEngine::trigger() |
Trigger types:
| Type | Description |
|---|---|
subscribe_to_list |
Fires when a subscriber confirms subscription to one of the selected lists |
custom_event |
Fires when a developer calls AutomationEngine::trigger('event-name', $subscriber) |
manual |
Never fires automatically — enqueue via code with AutomationEngine::enqueueForSubscriber() |
Steps
Each automation has ordered steps. A step = one email sent after a delay (in days).
| Field | Description |
|---|---|
| Name | Optional internal label |
| Delay (days) | Days after the trigger (0 = immediately on next worker run) |
| Template | Optional saved email template |
| Subject | Overrides the template subject if set |
| HTML body | Overrides the template body if set |
| From name | Overrides list/global sender if set |
| From email | Overrides list/global sender if set |
Priority rule: inline step fields override the template, which overrides the list defaults, which override mail.from.
When a trigger fires, the engine enqueues one item per step, each scheduled at trigger_time + cumulative delay.
Queue
Automation > Queue
Read-only view of all queue items. Useful for debugging delivery issues.
| Status | Meaning |
|---|---|
pending |
Waiting to be dispatched |
sent |
Dispatched and logged |
failed |
Worker threw an exception |
cancelled |
Subscriber unsubscribed before dispatch |
Reports
Automation > Reports
Global index — Total confirmed subscribers, new (30 days), emails sent (30 days), open rate (30 days). Tables for all automations and all lists with key stats.
Per-automation (/reports/automation/{id}) — Total sent / opened / clicked / pending. Per-step breakdown with open rate and click rate. 50 most recent send events.
Per-list (/reports/list/{id}) — Confirmed subscriber count, new today, emails sent and open rate across all automations linked to this list. Table of linked automations. 20 most recent confirmed subscribers.
Per-subscriber (/reports/subscriber/{id}) — Subscription history across all lists with statuses and timestamps. Full email history: automation, step, subject, sent time, opened time, click count, status.
Frontend component
automationSubscribeForm
Attach to any CMS page or layout to render a subscription form with double opt-in.
[automationSubscribeForm] listId = 1 allowListChoice = 0 successMessage = "Thank you! Check your email to confirm."
| Property | Type | Default | Description |
|---|---|---|---|
listId |
dropdown | — | Subscribe to this list. Falls back to the default list if empty |
allowListChoice |
checkbox | false | Renders checkboxes so the visitor can choose which lists to join |
selectableLists |
set | — | Which lists to display when allowListChoice is on. Leave empty to show all lists |
successMessage |
string | "Thank you! Please check your email to confirm your subscription." | Message shown after successful signup |
selectableLists is only used when allowListChoice is enabled. It lets you restrict the choice to a curated subset without exposing every list.
The form submits via AJAX. On success it replaces the form with the success message. Validation errors appear inline.
The default template (components/subscribeform/default.htm) is unstyled HTML. Override it from your theme. The updatable inner content lives in components/subscribeform/_form.htm — override that partial when you only need to restyle the form and success state.
Developer guide
Architecture overview
── subscribe_to_list trigger ──────────────────────────────────────────────────
SubscribeForm (component)
└── SubscriberManager::signup()
sends confirmation email
TrackingHandler::confirmSubscription()
└── SubscriberManager::confirm()
updates pivot status → fires inherent.automation.subscriber.confirmed
Event: inherent.automation.subscriber.confirmed
└── AutomationEngine::onSubscriberConfirmed()
enqueues steps → AutomationQueue
── custom_event trigger ───────────────────────────────────────────────────────
Any plugin
└── AutomationEngine::trigger('event-name', $subscriber)
looks up active automations by event name
enqueues steps → AutomationQueue (subscriber_list_id = null)
── dispatch (both paths) ──────────────────────────────────────────────────────
Scheduler (every minute)
└── php artisan automation:process
└── AutomationWorker::process()
dispatches due queue items
records AutomationLog with tracking code
Public routes
| Method | URL | Action |
|---|---|---|
| GET | automations/confirm/{token} |
Confirms a list subscription (double opt-in) |
| GET | automations/unsubscribe/{token} |
Unsubscribes via tracking code |
| POST | automations/unsubscribe/{token} |
One-Click unsubscribe (RFC 8058) |
| GET | automations/o/{code}.png |
Open tracking pixel |
| GET | automations/c/{code}?url=… |
Click tracking redirect |
Events
| Event | Payload | Fired when |
|---|---|---|
inherent.automation.subscriber.confirmed |
($subscriber, $list) |
Subscription confirmed via double opt-in or direct backend confirm |
inherent.automation.subscriber.unsubscribed |
($subscriber, $list\|null) |
Subscriber opts out. $list is null for custom_event triggered automations |
\Event::listen('inherent.automation.subscriber.confirmed', function ($subscriber, $list) {
// $list is always a SubscriberList instance here
});
\Event::listen('inherent.automation.subscriber.unsubscribed', function ($subscriber, $list) {
// $list may be null if the automation had no list context (custom_event trigger)
});
SubscriberManager API
use Inherent\Automation\Classes\SubscriberManager;
// Sign up from a form — creates subscriber + pivot, sends confirmation email
$subscriber = SubscriberManager::signup($email, [1, 3], [
'first_name' => 'Marie',
'last_name' => 'Dupont',
], $request->ip());
// Sign up from a RainLab User object
$subscriber = SubscriberManager::signupFromUser($user, [1]);
// Confirm directly — bypasses double opt-in (backend/admin action)
SubscriberManager::confirmDirect($subscriber, $list);
// Unsubscribe by tracking code (called by the unsubscribe route)
SubscriberManager::unsubscribeByTrackingCode($trackingCode);
signup() is idempotent: calling it twice with the same email only sends a new confirmation if the subscriber had previously unsubscribed.
Custom event trigger
Fire a named automation from any plugin:
use Inherent\Automation\Classes\AutomationEngine;
// Pass a Subscriber instance
AutomationEngine::trigger('order.placed', $subscriber);
// Pass an email string — throws \InvalidArgumentException if not found in DB
AutomationEngine::trigger('order.placed', 'customer@example.com');
In the backend, create an automation with trigger "When a custom event is triggered" and set the event name to order.placed. All active automations matching that slug will enqueue their steps for the subscriber.
The subscriber must already exist in the database. The method does not auto-create subscribers — the caller controls consent and opt-in.
Unsubscribe behaviour — with no list context, clicking the unsubscribe link cancels all pending queue items for that subscriber in the triggered automation. The subscriber's global status and other list memberships are unaffected.
Example (MelonCart / Mall)
// After a completed order
$subscriber = Subscriber::where('email', $order->customer_email)->first();
if ($subscriber) {
AutomationEngine::trigger('order.placed', $subscriber);
}
// After a loyalty milestone
if ($customer->completedOrdersCount() >= 5) {
AutomationEngine::trigger('loyalty.vip', $subscriber);
}
RainLab.User integration
When RainLab.User is installed, set rainlab_user_default_lists in the plugin config to auto-subscribe new registrations:
// config/config.php
'rainlab_user_default_lists' => [1, 2],
New users are signed up with a confirmation email sent to those list IDs.
Confirmation redirect
After a subscriber clicks the confirmation link, the plugin redirects to the URL defined by confirm_redirect in config/inherent/automation.php:
// config/inherent/automation.php
return [
'confirm_redirect' => env('AUTOMATION_CONFIRM_REDIRECT', '/email-confirmation'),
];
Set AUTOMATION_CONFIRM_REDIRECT in your .env to override without touching the config file:
AUTOMATION_CONFIRM_REDIRECT=/merci-de-votre-inscription
Defaults to /email-confirmation if the variable is absent.
Running automations manually
php artisan automation:process
Picks up all queue items with scheduled_at <= now() and status = pending, processed in batches of 50.
Extending with new trigger types
- Add the key to
Automation::getTriggerTypeOptions(). - Call
AutomationEngine::enqueueForSubscriber($automation, $subscriber, $list)from wherever the trigger fires.
\Event::listen('your.custom.event', function ($user) {
$subscriber = Subscriber::where('email', $user->email)->first();
if (!$subscriber) return;
Automation::active()
->where('trigger_type', 'your_trigger')
->with('steps')
->get()
->each(fn ($automation) => AutomationEngine::enqueueForSubscriber($automation, $subscriber));
});
Responsiv.Campaign integration
When the Responsiv Campaign plugin is installed, Inherent.Automation registers its subscriber lists as recipient groups inside Campaign. No configuration required — the integration activates automatically on boot.
Set the environment variable AUTOMATION_CAMPAIGN_INTEGRATION=true to enable this automatic integration. Per default, it is set to false by config.
What it does
Each confirmed-subscriber list appears in Campaign's message composer under a group named Automation : <list name>. Marketers can target these segments directly when sending a one-time newsletter or bulk campaign, without duplicating subscribers between the two plugins.
Only subscribers with a confirmed status on the list are exposed.
How it works
The plugin listens to two Campaign events:
| Event | Role |
|---|---|
responsiv.campaign.listRecipientGroups |
Declares each Automation list as a named recipient group |
responsiv.campaign.getRecipientsData |
Returns confirmed subscriber emails, first names and last names for the requested group |
Groups are keyed automation_list_{id} to avoid collisions with other Campaign recipient sources.
Enabling / disabling
The integration is off by default when Campaign is installed. Enable/Disable it via .env:
#AUTOMATION_CAMPAIGN_INTEGRATION=true AUTOMATION_CAMPAIGN_INTEGRATION=false
Or directly in config/inherent/automation.php:
'campaign_integration' => false,
Requirements
responsiv/campaigninstalled and active
Permissions
| Permission key | Description |
|---|---|
inherent.automation.manage_automations |
Create/edit automations and steps, view queue |
inherent.automation.manage_subscribers |
Manage subscribers and lists, view reports |
inherent.automation.manage_templates |
Create/edit email templates |
Changelog
1.0.4
automationSubscribeForm: newselectableListsproperty (typeset). WhenallowListChoiceis on, only the selected lists are shown to the visitor. Leave empty to keep the previous behaviour (all lists).automationSubscribeForm: fixed confirmation message not appearing after form submission. The AJAX update now returns the rendered inner partial directly from the handler, avoiding theonRun/ re-render conflict that resetsubscribedtofalse.- Responsiv.Campaign integration: when Campaign is installed, Automation subscriber lists are automatically registered as recipient groups in Campaign's message composer (confirmed subscribers only). Disable via
AUTOMATION_CAMPAIGN_INTEGRATION=falseorcampaign_integrationin config.
1.0.3
- New trigger type
custom_event: fire automations from any plugin viaAutomationEngine::trigger('event-name', $subscriber). - Fixed
unsubscribeByTrackingCode()for automations with no list context (subscriber_list_id = null): pending queue items are now cancelled per-automation instead of silently ignored. - Reports: per-automation report (step breakdown, open/click rates), per-list report (linked automations, recent subscribers), per-subscriber report (full history with subjects and click counts). Fixed open rate query on list report (was using
whereJsonContainson a non-existent JSON field). - Subscriber form: Lists tab now shows per-list pivot status, subscription date, and unsubscription date.
-
This plugin has not been reviewed yet.
-
| 1.0.2 |
Add inline email fields on automation steps (subject, content_html, from_name, from_email). email_template_id is now optional. Jun 29, 2026 |
|---|---|
| 1.0.1 |
Add trigger lists relation table (replaces JSON trigger_config for list_ids). Jun 29, 2026 |
| 1.0.0 |
First version of Inherent Automation plugin. Jun 29, 2026 |