A WooCommerce plugin for sending scheduled, branded emails to customers based on how long an order has been at a given status.
The original requirement
A client asked for something specific: automatically email customers whose orders had been in bacs-processing status for more than a set number of days, to remind them that payment was still outstanding. Then a follow-up after another threshold – something along the lines of “your account is on stop because this payment is overdue.”
What it does
Create email definitions in the admin area. Each definition says: “if an order has been in this status for at least this many days, send this email to this recipient.” The hourly WP cron evaluates all enabled definitions against matching orders and sends emails to orders that meet the conditions.
Each email is sent once per status period. If the order changes status (e.g. a BACS order moves from pending to cancelled) the sent-email log clears. If the order later returns to pending, the definitions can trigger again from scratch.
Use cases
BACS payment reminders. The original use case. Set up two definitions targeting bacs-processing: one at 3 days (a polite nudge) and one at 7 days (a firmer “your account is on stop” message). The 3-day email fires once, and if the order is still unpaid at day 7, the second email fires.
Review requests. Target completed status with a threshold of 14 days. Send a personalised email with the order details and a link back to the site, asking for a product review.
Anything else. Any WooCommerce order status, any threshold, any email content. Custom statuses registered by other plugins are included automatically.
Setting up email definitions
Navigate to WooCommerce > Order Status Emails and click Add New.
Each definition has:
- Label: an internal name for the definition (not shown to customers)
- Status: the WooCommerce order status to target
- Days at status: how many whole days the order must have been at that status before the email fires
- Subject: the email subject line, with token support
- Body: the email content, with token support
- Recipient: customer billing email, site admin email, or a fixed custom address
- Enabled/disabled
Save the definition and it starts being evaluated on the next hourly cron run.
Token substitution
Email subjects and bodies support moustache-style {{token}} placeholders that are replaced with order/customer data when sending an the email.
Built-in tokens:
{{customer.first_name}}— customer first name{{customer.last_name}}— customer last name{{customer.full_name}}— customer full name{{customer.email}}— customer email address{{customer.company}}— customer company name{{order.id}}— order number{{order.total}}— formatted order total{{order.date}}— order date{{order.items}}— WooCommerce-styled order line items table (HTML){{order.view_url}}— link to the order in My Account{{order.payment_url}}— direct link to the WooCommerce checkout payment page for this order{{order.status}}— human-readable status label{{order.payment_method}}— payment method name{{site.name}}— site name{{site.url}}— site URL
The {{order.payment_url}} token is particularly useful for Pending Payment reminders — you can include a direct “pay now” link in the email without the customer needing to navigate through My Account.
Custom meta tokens
Use the Settings tab to add custom meta keys to use as tokens. Enter one key per line under Order meta keys or Customer meta keys, and the token becomes available in all email definitions as {{order.meta.KEY}} or {{customer.meta.KEY}}.
For example, if orders have a _purchase_order_number meta field, add it to the order meta keys list and use {{order.meta._purchase_order_number}} in your email body. The token reference in the edit form updates automatically to include the keys you’ve configured.
How the cron stuff works
For each hourly cron trigger…
- Check whether the current time falls within the configured send schedule (see below)
- Load all enabled email definitions
- For each definition, query WooCommerce for orders at the target status
- For each matching order, calculate how many whole days it has been at its current status
- Skip the order if the days threshold hasn’t been met
- Skip the order if this definition has already been sent for the current status period
- Send the email if all conditions are met, then records it as sent in the order meta
Important: Orders created before the plugin was activated won’t have the core meta data, so they won’t be eligible for reminder emails. Only orders created after the plugin was installed will be eligible.
When an email is sent successfully, we create an order note: Payment Email Notifications: "Definition Name" sent to customer@example.com. If the email fails, we log it to the PHP error log.
Restrict email sending to a time window
On the admin settings tab…
Time-of-day window: Enable this to restrict sending to a specific time range — for example, 09:00 to 17:00. The cron runs every hour, but if the current server time is outside the window, the entire run is skipped. The settings page shows your server’s configured timezone so there’s no ambiguity.
Day-of-week restrictions: Enable this to select which days sending is allowed. If today isn’t in the list, the cron run is skipped.
Both constraints can be used together. If you want BACS reminders to only go out on weekday mornings (not at 2am on a Sunday) this is how you do it.
Extending the plugin
Four filter hooks are available for developers who need to customise behaviour.
Add or modify tokens
add_filter( 'pen_email_tokens', function( $tokens, $order, $template ) {
// Add a custom token from an order meta field.
$tokens['order.account_manager'] = $order->get_meta( '_account_manager_name' );
return $tokens;
}, 10, 3 );Filter email content before sending
add_filter( 'pen_email_content', function( $body_html, $definition_id, $definition, $order ) {
// Append a custom footer to a specific definition.
if ( 'bacs-reminder-7-day' === $definition_id ) {
$body_html .= '<p>If you have any questions, please call us on 01234 567890.</p>';
}
return $body_html;
}, 10, 4 );Override the recipient
add_filter( 'pen_email_recipient', function( $recipient, $definition_id, $definition, $order ) {
// For wholesale orders, CC the account manager instead.
if ( $order->get_meta( '_is_wholesale' ) ) {
return get_option( 'wholesale_manager_email' );
}
return $recipient;
}, 10, 4 );Conditionally suppress an email
add_filter( 'pen_should_send_email', function( $should_send, $definition_id, $definition, $order ) {
// Don't send BACS reminders to customers on a payment plan.
if ( $order->get_meta( '_on_payment_plan' ) ) {
return false;
}
return $should_send;
}, 10, 4 );
