docs/extensions/settings-and-config/email-editor-integration.md
This guide shows how extensions can add custom email notifications that integrate with the WooCommerce Email Editor.
Note: The WooCommerce Email Editor is currently in alpha. To enable it, go to WooCommerce > Settings > Advanced > Features and enable Block Email Editor (alpha).
WC_Email – Create a custom email class for your notification by extending the core WooCommerce email class.woocommerce_email_classes – Add your new email class to WooCommerce so it appears in the admin email settings.woocommerce_transactional_emails_for_block_editor filter to enable block editor support.Extend WC_Email and implement the required methods:
class YourPlugin_Custom_Email extends WC_Email {
public function __construct() {
$this->id = 'your_plugin_custom_email';
$this->title = __( 'Custom Email', 'your-plugin' );
$this->customer_email = true;
$this->email_group = 'your-plugin';
$this->template_html = 'emails/your-custom-email.php';
$this->template_plain = 'emails/plain/your-custom-email.php';
$this->template_base = plugin_dir_path( __FILE__ ) . 'templates/';
parent::__construct();
}
public function get_default_subject() {
return __( 'Your custom email subject', 'your-plugin' );
}
public function get_default_heading() {
return __( 'Your custom email heading', 'your-plugin' );
}
public function trigger( $order_id ) {
$this->setup_locale();
if ( $order_id ) {
$order = wc_get_order( $order_id );
$this->object = $order;
$this->recipient = $order->get_billing_email();
}
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
public function get_content_html() {
return wc_get_template_html( $this->template_html, array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
) );
}
public function get_content_plain() {
return wc_get_template_html( $this->template_plain, array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
) );
}
}
Add your email to WooCommerce:
// Add the custom email class to the WooCommerce Emails.
function your_plugin_add_email_class( $email_classes ) {
$email_classes['YourPlugin_Custom_Email'] = new YourPlugin_Custom_Email();
return $email_classes;
}
add_filter( 'woocommerce_email_classes', 'your_plugin_add_email_class' );
// Add the custom email group. This is only necessary if email_group is not set on the WC_Email class.
function your_plugin_add_email_group( $email_groups ) {
$email_groups['your-plugin'] = __( 'Your Plugin', 'your-plugin' );
return $email_groups;
}
add_filter( 'woocommerce_email_groups', 'your_plugin_add_email_group' );
Third-party extensions need to explicitly opt their emails into block editor support. This is done by registering your email ID with the woocommerce_transactional_emails_for_block_editor filter:
/**
* Register custom transactional emails for the block editor.
*
* @param array $emails Array of email IDs.
* @return array Modified array of email IDs.
*/
function your_plugin_register_transactional_emails_for_block_editor( $emails ) {
$emails[] = 'your_plugin_custom_email';
return $emails;
}
add_filter( 'woocommerce_transactional_emails_for_block_editor', 'your_plugin_register_transactional_emails_for_block_editor' );
Important: Without this step, your email may still appear in the email list, but it will not use the email editor, as explicit opt-in is required from third-party developers.
Note: For third-party extensions, WooCommerce will not create an email post unless you opt-in using the woocommerce_transactional_emails_for_block_editor filter.
Development tip: WooCommerce caches email post-generation with a transient. When testing or developing, delete the transient wc_email_editor_initial_templates_generated to force post-generation.
You can modify the email template post data before it's created using the woocommerce_email_content_post_data filter. This allows you to customize the post title, content, meta, or any other post data during template generation.
Filter details:
| Property | Value |
|---|---|
| Hook name | woocommerce_email_content_post_data |
| Since | 10.5.0 |
| Parameters | $post_data (array), $email_type (string), $email_data (\WC_Email) |
| Returns | array |
Parameters:
$post_data (array) – The post data array that will be passed to wp_insert_post(). Contains keys like post_type, post_status, post_title, post_content, post_excerpt, post_name, and meta_input.$email_type (string) – The email type identifier (e.g., 'customer_processing_order').$email_data (\WC_Email) – The WooCommerce email object.Return value:
Return the modified post data array. This array will be used to create the email template post.
/**
* Customize email template post data during generation.
*
* @param array $post_data The post data array.
* @param string $email_type The email type identifier.
* @param \WC_Email $email_data The WooCommerce email object.
* @return array Modified post data.
*/
function your_plugin_customize_email_template_post( $post_data, $email_type, $email_data ) {
// Modify the post title for specific email types.
if ( 'customer_processing_order' === $email_type ) {
$post_data['post_title'] = __( 'Custom Processing Order Email', 'your-plugin' );
}
// Modify the post content (block template HTML).
$post_data['post_content'] = str_replace(
'default content',
'custom content',
$post_data['post_content']
);
// Add custom meta data.
$post_data['meta_input']['custom_meta_key'] = 'custom_value';
return $post_data;
}
add_filter( 'woocommerce_email_content_post_data', 'your_plugin_customize_email_template_post', 10, 3 );
Important notes:
wp_insert_post() parameter (post_title, post_content, post_excerpt, post_status, post_name, meta_input, etc.).$post_data array.post_content, ensure valid block markup is maintained.$email_type to target specific emails.Create templates/emails/block/your-custom-email.php:
Template base property: Make sure to set the $template_base property in your email class constructor to point to your plugin's template directory. This allows WooCommerce to properly locate and load your block template files. The block template filename is expected to match the plain template, but using the block directory instead of plain.
<?php
use Automattic\WooCommerce\Internal\EmailEditor\BlockEmailRenderer;
defined( 'ABSPATH' ) || exit;
?>
<!-- wp:heading -->
<h2 class="wp-block-heading"><?php printf( esc_html__( 'Hello %s!', 'your-plugin' ), '<!--[woocommerce/customer-first-name]-->' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p><?php printf( esc_html__( 'Thank you for your order #%s.', 'your-plugin' ), '<!--[woocommerce/order-number]-->' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:woocommerce/email-content {"lock":{"move":false,"remove":true}} -->
<div class="wp-block-woocommerce-email-content"><?php echo esc_html( BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER ); ?></div>
<!-- /wp:woocommerce/email-content -->
Pro tip: If you use a custom path for your email templates, set the block template path using the template_block property on the email class.
Email content placeholder:
The BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER is a special placeholder that gets replaced with the main email content when the email is rendered. This placeholder is essential for integrating with WooCommerce's email system and allows the email editor to inject the core email content (like order details, customer information, etc.) into your custom template.
By default, WooCommerce uses the general block email template to generate the content that replaces this placeholder. When WooCommerce processes your email template, it replaces this placeholder with the appropriate email content based on the email type and context.
If your email needs to use different content, you have two options:
Using custom block content template:
Set a custom template: Set the $template_block_content property in your email class constructor to point to a custom template for the block content:
$this->template_block_content = 'emails/block/custom-content.php';
Implement custom logic: Implement the get_block_editor_email_template_content method in your email class to provide custom logic for generating the content:
public function get_block_editor_email_template_content() {
return '<!-- wp:paragraph -->
<p>Your custom block template content here</p>
<!-- /wp:paragraph -->';
}
Using action hook:
You can use the action hook woocommerce_email_general_block_email to execute additional actions within the content template.
Set up when your email should be sent by hooking into WordPress actions. You can trigger emails on WooCommerce events or your own custom actions:
function your_plugin_trigger_custom_email( $order_id ) {
$emails = WC()->mailer()->get_emails();
$email = $emails['YourPlugin_Custom_Email'];
$email->trigger( $order_id );
}
// Trigger on WooCommerce order completion
add_action( 'woocommerce_order_status_completed', 'your_plugin_trigger_custom_email' );
// Trigger on your custom plugin action
add_action( 'your_plugin_custom_action', 'your_plugin_trigger_custom_email' );
Common WooCommerce hooks you can use:
woocommerce_order_status_completed - When order is completedwoocommerce_order_status_processing - When order is processingwoocommerce_new_order - When new order is createdwoocommerce_customer_created - When new customer registersPersonalization tags allow you to insert dynamic content into your emails. They appear as <!--[tag-name]--> in your templates and get replaced with actual values when the email is sent.
WooCommerce provides many built-in personalization tags organized by category:
<!--[woocommerce/customer-email]--> - Customer's email address<!--[woocommerce/customer-first-name]--> - Customer's first name<!--[woocommerce/customer-last-name]--> - Customer's last name<!--[woocommerce/customer-full-name]--> - Customer's full name<!--[woocommerce/customer-username]--> - Customer's username<!--[woocommerce/customer-country]--> - Customer's country<!--[woocommerce/order-number]--> - Order number<!--[woocommerce/order-date]--> - Order date (supports format parameter)<!--[woocommerce/order-items]--> - List of order items<!--[woocommerce/order-subtotal]--> - Order subtotal<!--[woocommerce/order-tax]--> - Order tax amount<!--[woocommerce/order-discount]--> - Order discount amount<!--[woocommerce/order-shipping]--> - Order shipping cost<!--[woocommerce/order-total]--> - Order total amount<!--[woocommerce/order-payment-method]--> - Payment method used<!--[woocommerce/order-payment-url]--> - Payment URL for order<!--[woocommerce/order-transaction-id]--> - Transaction ID<!--[woocommerce/order-shipping-method]--> - Shipping method used<!--[woocommerce/order-shipping-address]--> - Formatted shipping address<!--[woocommerce/order-billing-address]--> - Formatted billing address<!--[woocommerce/order-view-url]--> - Customer order view URL<!--[woocommerce/order-admin-url]--> - Admin order edit URL<!--[woocommerce/order-custom-field]--> - Custom order field (requires key parameter)<!--[woocommerce/site-title]--> - Site title<!--[woocommerce/site-homepage-url]--> - Homepage URL<!--[woocommerce/store-email]--> - Store email address<!--[woocommerce/store-url]--> - Store URL<!--[woocommerce/store-name]--> - Store name<!--[woocommerce/store-address]--> - Store address<!--[woocommerce/my-account-url]--> - My Account page URL<!--[woocommerce/admin-order-note]--> - Admin order noteCreate your own tags for plugin-specific data using the proper WooCommerce hook:
/**
* Register custom personalization tags for the email editor.
*
* @param \Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry $registry The registry.
* @return \Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry
*/
function your_plugin_register_personalization_tags( $registry ) {
// Register custom field tag
$custom_field_tag = new \Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tag(
// Display name in editor
__( 'Custom Field', 'your-plugin' ),
// Token (unique identifier)
'your-plugin/custom-field',
// Category for grouping
__( 'Your Plugin Group', 'your-plugin' ),
// Callback function
'your_plugin_get_custom_field_value',
// Attributes (optional)
array(),
// Value to insert (optional - defaults to token)
null,
// Post types this tag works with
array( 'woo_email' )
);
$registry->register( $custom_field_tag );
return $registry;
}
// Callback function that returns the custom field value
function your_plugin_get_custom_field_value( $context, $args = array() ) {
$order_id = $context['order']->get_id() ?? 0;
return get_post_meta( $order_id, '_custom_field', true );
}
// Register with the proper WooCommerce hook
add_filter( 'woocommerce_email_editor_register_personalization_tags', 'your_plugin_register_personalization_tags' );
Usage in templates: Use <!--[your-plugin/custom-field]--> in your block template, and it will be replaced with the value returned by your callback function.
To learn more about personalization tags, please see the personalization tags documentation in the woocommerce/email-editor package.
Use the woocommerce_email_editor_integration_personalizer_context_data filter to provide custom context data to your personalization tags. This is useful when your extension needs to pass additional data (such as subscription details, loyalty points, or custom order metadata) that your personalization tag callbacks can access.
Filter details:
| Property | Value |
|---|---|
| Hook name | woocommerce_email_editor_integration_personalizer_context_data |
| Since | 10.5.0 |
| Parameters | $context (array), $email (\WC_Email) |
| Returns | array |
Parameters:
$context (array) – The existing context data array. This may already contain data from WooCommerce core or other extensions.$email (\WC_Email) – The WooCommerce email object being processed. You can use this to access the email ID, recipient, and the object associated with the email (such as an order or customer).Return value:
Return an array of custom context data along with Woo core context data. This array will be accessible to all personalization tag callbacks through the $context parameter.
/**
* Add subscription-related context data for personalization tags.
*
* @param array $context The existing context data.
* @param \WC_Email $email The WooCommerce email object.
* @return array Modified context data.
*/
function your_plugin_add_subscription_context( $context, $email ) {
// Only add context for subscription-related emails.
if ( strpos( $email->id, 'subscription' ) === false ) {
return $context;
}
// Get the order from the email object.
$order = $email->object instanceof WC_Order ? $email->object : null;
if ( ! $order ) {
return $context;
}
// Add your custom subscription data to context.
$context['subscription_id'] = $order->get_meta( '_subscription_id' );
$context['subscription_end_date'] = $order->get_meta( '_subscription_end_date' );
$context['renewal_count'] = (int) $order->get_meta( '_renewal_count' );
return $context;
}
add_filter( 'woocommerce_email_editor_integration_personalizer_context_data', 'your_plugin_add_subscription_context', 10, 2 );
Once you've added custom data to the context, your personalization tag callbacks can access it:
/**
* Personalization tag callback that uses custom context data.
*
* @param array $context The context data (includes your custom data).
* @param array $args Optional attributes passed to the tag.
* @return string The personalized value.
*/
function your_plugin_get_subscription_end_date( $context, $args = array() ) {
// Access the custom context data you added via the filter.
$end_date = $context['subscription_end_date'] ?? '';
if ( empty( $end_date ) ) {
return __( 'N/A', 'your-plugin' );
}
// Format the date according to site settings.
return date_i18n( get_option( 'date_format' ), strtotime( $end_date ) );
}
Important notes:
$email->object property typically contains the main object associated with the email (e.g., WC_Order for order emails, WP_User for user-related emails).esc_html(), esc_attr(), esc_url()).Below is an example of a loyalty program welcome email implementation:
Email Class:
class YourPlugin_Loyalty_Welcome_Email extends WC_Email {
public function __construct() {
$this->id = 'loyalty_welcome_email';
$this->title = __( 'Loyalty Welcome Email', 'your-plugin' );
$this->customer_email = true;
$this->email_group = 'loyalty';
$this->template_html = 'emails/loyalty-welcome.php';
$this->template_plain = 'emails/plain/loyalty-welcome.php';
$this->template_block = 'emails/block/loyalty-welcome.php';
$this->template_base = plugin_dir_path( __FILE__ ) . 'templates/';
parent::__construct();
}
public function get_default_subject() {
return __( 'Welcome to our Loyalty Program!', 'your-plugin' );
}
public function trigger( $customer_id, $points_earned = 0 ) {
$this->setup_locale();
$customer = new WC_Customer( $customer_id );
$this->object = $customer;
$this->recipient = $customer->get_email();
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
public function get_content_html() {
return wc_get_template_html( $this->template_html, array(
'customer' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
) );
}
public function get_content_plain() {
return wc_get_template_html( $this->template_plain, array(
'customer' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
) );
}
}
Block Email:
<!-- wp:heading -->
<h2 class="wp-block-heading"><?php printf( esc_html__( 'Welcome %s!', 'your-plugin' ), '<!--[woocommerce/customer-first-name]-->' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p><?php esc_html_e( 'Thank you for joining our loyalty program!', 'your-plugin' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:woocommerce/email-content {"lock":{"move":false,"remove":true}} -->
<div class="wp-block-woocommerce-email-content"><?php echo esc_html( BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER ); ?></div>
<!-- /wp:woocommerce/email-content -->
Registration and Setup:
This code ties everything together - registering the email class, template, and trigger:
// Add the custom email class to the WooCommerce Emails.
add_filter( 'woocommerce_email_classes', function( $classes ) {
$classes['YourPlugin_Loyalty_Welcome_Email'] = new YourPlugin_Loyalty_Welcome_Email();
return $classes;
} );
// Add the custom email group.
add_filter( 'woocommerce_email_groups', function( $email_groups ) {
$email_groups['loyalty'] = __( 'Loyalty Program', 'your-plugin' );
return $email_groups;
} );
// Register the email with the block editor.
add_filter( 'woocommerce_transactional_emails_for_block_editor', function( $emails ) {
$emails[] = 'loyalty_welcome_email';
return $emails;
} );
// Set up trigger - when to send the email
add_action( 'your_plugin_customer_joined_loyalty', function( $customer_id, $points_earned ) {
$emails = WC()->mailer()->get_emails();
$email = $emails['YourPlugin_Loyalty_Welcome_Email'];
$email->trigger( $customer_id, $points_earned );
}, 10, 2 );
How it works:
woocommerce_email_classes filter and that the class name is correct.woocommerce_transactional_emails_for_block_editor filter.Your custom email will now be available in WooCommerce > Settings > Emails and can be edited using the block editor.