deployecommerce / module-order-status-email
Send any email template to the customer when an order reaches a chosen status. Rules are managed in the admin, scoped per website or store view, with a subject and template per rule.
Package info
github.com/DeployEcommerce/module-order-status-email
Type:magento2-module
pkg:composer/deployecommerce/module-order-status-email
Requires
- php: ~8.2.0||~8.3.0||~8.4.0
- deployecommerce/module-order-status-transition: ~1.0.0
- magento/framework: >=103.0.0 <104
- magento/module-backend: >=102.0.0 <103
- magento/module-config: >=101.2.0 <102
- magento/module-email: >=101.1.0 <102
- magento/module-sales: >=103.0.0 <104
- magento/module-store: >=101.1.0 <102
- magento/module-ui: >=101.2.0 <102
Requires (Dev)
- pestphp/pest: ^2.35 || ^3.7
README
Send any email template to the customer when an order reaches a chosen status.
Magento can send an email when an order is placed, invoiced, shipped or credited. It cannot send one when an order reaches your status — the "Dispatched", "Ready for Collection" or "Awaiting Survey" that your business actually runs on. This module fills that gap with rules you manage in the admin rather than code.
composer require deployecommerce/module-order-status-email
bin/magento module:enable DeployEcommerce_OrderStatusEmail
bin/magento setup:upgrade
Rules
Marketing → Communications → Order Status Emails
Each rule says: in this scope, when an order reaches this status, send this template with this subject.
| Field | Notes |
|---|---|
| Scope | Default, a website, or a store view |
| Order Status | Every status defined on your store, read from the database — including your own |
| Email Template | Anything under Marketing → Email Templates, or a template registered by a module |
| Subject | The email's subject line. Also labels the rule in the grid, e.g. "Dispatch Confirmation" |
| Send From | Which store email identity to send as |
| Active | Turn a rule off without deleting it |
| Sort Order | The order rules sharing a scope and status are queued in |
How scope is decided
The most specific match wins outright, the same way store configuration works. A store view rule beats a website rule, which beats a default rule — and only the winning tier is sent.
That is what makes "turn this on for the US store only" a single row. If every matching scope fired instead, adding a default rule later would silently start double-emailing the US store.
Several rules may share the winning tier, and all of them fire, in sort order. Two emails at one status is a legitimate thing to want.
A rule with no template sends nothing
Leaving the template as Do Not Send keeps the rule and its history but sends no email. This is checked when the email is queued and again when it is sent, so clearing a template stops emails that are already in the queue.
Templates
Any template works. Every one gets the same variables:
| Variable | |
|---|---|
order, order_id, store |
The order and its store |
order_data.* |
customer_name, customer_email, customer_is_guest, is_not_virtual, email_customer_note, frontend_status_label |
billing, formattedBillingAddress, formattedShippingAddress |
Named as core's sales emails name them, so core markup can be pasted straight in |
shipment, shipments, tracks |
May be empty — see below |
email_subject |
The subject from the rule |
rule_status |
The status that triggered the email |
year |
The current year |
Variables are assembled inside store emulation for the order's store view, so locale, currency, address format and theme all resolve correctly even though the email is sent from cron.
Two things to get right in your template
The subject. Put {{var email_subject}} in the template's own subject line so the rule's subject
is used:
<!--@subject {{var email_subject}} @-->
Without it the template's fixed subject wins and the rule's subject only labels the grid.
Shipments can be missing. An order can reach a "dispatched" status with no shipment record — because someone set the status by hand, or an import wrote it. Always guard:
{{depend shipment}}
{{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order area="frontend"}}
{{/depend}}
A worked example covering all of this ships at
view/frontend/email/order_status_change.html, registered as Order Status Change (example).
Copy it under Marketing → Email Templates and edit your copy there.
Adding your own variables
<type name="DeployEcommerce\OrderStatusEmail\Model\TemplateVars\CompositeProvider"> <arguments> <argument name="providers" xsi:type="array"> <item name="loyalty" sortOrder="30" xsi:type="object">My\Module\Model\LoyaltyVars</item> </argument> </arguments> </type>
Implement DeployEcommerce\OrderStatusEmail\Api\TemplateVarsProviderInterface. A provider that
throws costs you its own variables and is logged; the email still goes out.
There is also an event, deployecommerce_order_status_email_set_template_vars_before, if you need
to change variables for one specific order rather than for every email.
How sending works
An order status change queues a row; a cron job every minute sends it. Nothing is sent inside the request that changed the status.
That matters more than the minute of latency costs you. Sending inline would put the mail provider's HTTP latency inside admin saves, inside payment webhook handlers — which time out and retry the whole webhook — and inside checkout. It would also mean a provider outage silently loses emails, because a failure at that point in the request has nowhere to go but the log.
Instead:
- The queue row is written inside the order's own database transaction, so it exists if and only if the status change was committed.
- A unique key over
(order_id, rule_id)makes duplicates impossible — across concurrent web nodes, a double-clicked admin save, or a redelivered payment webhook. - Failures are retried, up to Maximum Send Attempts, and keep their last error.
- Everything is re-validated at send time. If the order moved back out of the status, or the rule was turned off, the row is marked skipped rather than sent.
One email per order per rule, permanently. An order that leaves the status and returns to it does not produce a second email.
Configuration
Stores → Configuration → Sales → Order Status Emails
| Setting | Default | |
|---|---|---|
| Enabled | Yes | Per-scope kill switch, without editing rules |
| Send Copy To | — | Comma-separated. Sent as separate emails, not blind copies, because some transports forward only the first BCC address |
| Emails Per Cron Run | 50 | |
| Maximum Send Attempts | 3 | |
| Keep Sent Records For | 90 days | Only sent rows are pruned; failed and skipped rows are kept |
Requirements
Requires deployecommerce/module-order-status-transition,
which does the work of noticing a status change reliably. See its README for what is and is not
detected — in particular, status changes written straight to the database by bulk import tools are
invisible to any event-based approach.
Development
composer install
composer test
Tests are written with Pest and need no Magento installation. Magento's own packages come from the public Mage-OS mirror, so no Adobe credentials are required.
To work on this and the transition package together, point Composer at your local checkout:
composer config repositories.transition path ../module-order-status-transition
That is a local convenience only — do not commit it.
Licence
MIT. See LICENSE.md.