This week, I was asked by another user of the FlowMattic Facebook group if I had ever used Flowmattic to do an abandoned cart workflow. I hadn’t and actually hadn’t thought about it, but the introduction of the recent email builder means projects like this are now possible, and they will look great too.
So I set myself a challenge to get a working proof of concept within a few hours, and I was quite pleased with the result.
Please Note: The code in this article works and does what I intended it allows for a trigger and follow up it is not battle tested it is a proof of concept
Now, the first thing I needed to do was find a trigger or action that I could hook into or use to trigger our workflow. Looking at the documentation, I couldn’t find a native solution, and the plugins I could find were not adding the functionality; they were about selling full cart abandonment capabilities.
Abandoned cart workflow
So I began reading up on cart abandonment theory so I could understand what my custom trigger would do, and settled on a workflow that includes multiple reminders and a discount code After all, FlowMattic is capable of all these things

With that now finalised, I began working on the custom trigger and CRON workflow and this resulted in the custom code below;
<?php
/**
* To add a 72-hour follow-up, copy the entire 24-hour block below:
*
* // 2b) 24-hour handler: send second follow-up & schedule 48h
* add_action( 'my_send_abandoned_cart_payload_24h', 'send_abandoned_cart_payload_24h', 10, 1 );
* function send_abandoned_cart_payload_24h( $session_id ) {
* if ( $payload = get_transient( 'abandoned_cart_' . $session_id ) ) {
* do_action( 'my_abandoned_cart_workflow_step2', $payload );
* // schedule 48-h touch
* if ( ! wp_next_scheduled( 'my_send_abandoned_cart_payload_48h', [ $session_id ] ) ) {
* wp_schedule_single_event( time() + 2 * DAY_IN_SECONDS, 'my_send_abandoned_cart_payload_48h', [ $session_id ] );
* }
* }
* }
*
* Then modify it as follows to create your 72-hour handler:
*
* 1. Change the hook name from 'my_send_abandoned_cart_payload_24h' to 'my_send_abandoned_cart_payload_72h'
* 2. Change the function name accordingly (e.g. send_abandoned_cart_payload_72h)
* 3. In the scheduling call, use time() + 3 * DAY_IN_SECONDS
* 4. Fire your step-4 workflow action, e.g. do_action( 'my_abandoned_cart_workflow_step4', $payload );
*
* Example:
*
* // 2c) 72-hour handler: send fourth follow-up
* add_action( 'my_send_abandoned_cart_payload_72h', 'send_abandoned_cart_payload_72h', 10, 1 );
* function send_abandoned_cart_payload_72h( $session_id ) {
* if ( $payload = get_transient( 'abandoned_cart_' . $session_id ) ) {
* do_action( 'my_abandoned_cart_workflow_step4', $payload );
* }
* }
*/
// 1) On every cart update: only when the cart hash changes do we cache & schedule the first touch
add_action( 'woocommerce_cart_updated', 'schedule_abandoned_cart_payload_with_hash', 20 );
function schedule_abandoned_cart_payload_with_hash() {
//WC()->session->set( 'last_cart_hash', '' );
//error_log( '[ABANDON DEBUG] schedule_abandoned_cart_payload_with_hash fired' );
if ( WC()->cart->is_empty() ) return;
if ( ! WC()->session->has_session() ) {
WC()->session->init();
}
// compute cart-hash
$parts = array_map( fn( $i ) => $i['product_id'].'×'.$i['quantity'], WC()->cart->get_cart() );
$cart_hash = md5( implode( '|', $parts ) );
$last_hash = WC()->session->get( 'last_cart_hash' );
if ( $last_hash && $last_hash === $cart_hash ) return;
WC()->session->set( 'last_cart_hash', $cart_hash );
// build & cache payload
$session_id = WC()->session->get_customer_unique_id();
$user_id = get_current_user_id() ?: 0;
$user = $user_id ? get_userdata( $user_id ) : null;
$now = current_time( 'mysql' );
$detailed_items = array_values( array_map( 'build_item_payload', WC()->cart->get_cart() ) );
$cart_totals = WC()->cart->get_totals();
$applied_coupons = WC()->cart->get_applied_coupons();
$fees = WC()->cart->get_fees();
$shipping_methods = WC()->shipping()->get_packages()[0]['rates'] ?? [];
$payload = [
'user_id' => $user_id,
'user_email' => $user ? $user->user_email : '',
'user_name' => $user ? trim($user->first_name.' '.$user->last_name) : '',
'user_roles' => $user ? $user->roles : [],
'session_id' => $session_id,
'abandoned_at' => $now,
'ip_address' => WC_Geolocation::get_ip_address(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
//'items' => $detailed_items,
'items' => wp_json_encode( $detailed_items ),
'cart_subtotal' => wc_format_decimal( $cart_totals['subtotal'], 2 ),
'cart_discount' => wc_format_decimal( $cart_totals['discount_total'], 2 ),
'cart_tax' => wc_format_decimal( $cart_totals['total_tax'] ?? 0, 2 ),
'cart_shipping' => wc_format_decimal( $cart_totals['shipping_total'], 2 ),
'cart_total' => wc_format_decimal( $cart_totals['total'], 2 ),
'applied_coupons'=> $applied_coupons,
'fees' => array_map( fn($fee)=>['name'=>$fee->name,'amount'=>wc_format_decimal($fee->amount,2)], $fees ),
'shipping_methods'=> array_map( fn($r)=>['id'=>$r->get_method_id(),'label'=>$r->get_label(),'cost'=>wc_format_decimal($r->get_cost(),2)], $shipping_methods ),
'currency' => get_woocommerce_currency(),
'landing_page' => wp_get_referer() ?: '',
'utm' => [
'source'=>$_COOKIE['utm_source']??'',
'medium'=>$_COOKIE['utm_medium']??'',
'campaign'=>$_COOKIE['utm_campaign']??'',
],
];
set_transient( 'abandoned_cart_' . $session_id, $payload, 3 * HOUR_IN_SECONDS );
//error_log( "[ABANDON DEBUG] set_transient abandoned_cart_{$session_id}" );
// schedule 30-min touch
if ( ! wp_next_scheduled( 'my_send_abandoned_cart_payload', [ $session_id ] ) ) {
wp_schedule_single_event( time() + MINUTE_IN_SECONDS * 30, 'my_send_abandoned_cart_payload', [ $session_id ] );
//error_log( "[ABANDON DEBUG] scheduled my_send_abandoned_cart_payload for session {$session_id}" );
}
}
// helper to build each line-item
function build_item_payload( $item ) {
$p = $item['data'];
$categories = wp_get_post_terms( $p->get_id(), 'product_cat', ['fields'=>'names'] );
return [
'product_id'=> $p->get_id(),
'sku' => $p->get_sku(),
'product_name'=>$p->get_name(),
'quantity' => $item['quantity'],
'price' => wc_format_decimal($p->get_price(),2),
'line_subtotal'=>wc_format_decimal($item['line_subtotal'],2),
'line_total'=>wc_format_decimal($item['line_total'],2),
'categories'=> $categories,
'permalink' => $p->get_permalink(),
'image' => wp_get_attachment_url($p->get_image_id()),
];
}
// 2a) 30-minute handler: send first follow-up & schedule 24h
add_action( 'my_send_abandoned_cart_payload', 'send_abandoned_cart_payload_30min', 10, 1 );
function send_abandoned_cart_payload_30min( $session_id ) {
if ( $payload = get_transient( 'abandoned_cart_' . $session_id ) ) {
do_action( 'my_abandoned_cart_workflow_step1', $payload );
// schedule 24-h touch
if ( ! wp_next_scheduled( 'my_send_abandoned_cart_payload_24h', [ $session_id ] ) ) {
wp_schedule_single_event( time() + DAY_IN_SECONDS, 'my_send_abandoned_cart_payload_24h', [ $session_id ] );
}
}
}
// 2b) 24-hour handler: send second follow-up & schedule 48h
add_action( 'my_send_abandoned_cart_payload_24h', 'send_abandoned_cart_payload_24h', 10, 1 );
function send_abandoned_cart_payload_24h( $session_id ) {
if ( $payload = get_transient( 'abandoned_cart_' . $session_id ) ) {
do_action( 'my_abandoned_cart_workflow_step2', $payload );
// schedule 48-h touch
if ( ! wp_next_scheduled( 'my_send_abandoned_cart_payload_48h', [ $session_id ] ) ) {
wp_schedule_single_event( time() + 2 * DAY_IN_SECONDS, 'my_send_abandoned_cart_payload_48h', [ $session_id ] );
}
}
}
// 2c) 48-hour handler: send third follow-up
add_action( 'my_send_abandoned_cart_payload_48h', 'send_abandoned_cart_payload_48h', 10, 1 );
function send_abandoned_cart_payload_48h( $session_id ) {
if ( $payload = get_transient( 'abandoned_cart_' . $session_id ) ) {
do_action( 'my_abandoned_cart_workflow_step3', $payload );
}
}
// 3) Cleanup on checkout: delete payload & unschedule all touches
add_action( 'woocommerce_checkout_order_processed', 'cleanup_abandoned_cart_on_checkout', 10, 3 );
function cleanup_abandoned_cart_on_checkout( $order_id, $posted_data, $order ) {
if ( ! WC()->session->has_session() ) WC()->session->init();
$sid = WC()->session->get_customer_unique_id();
if ( ! $sid ) return;
delete_transient( 'abandoned_cart_' . $sid );
// hooks to remove
$hooks = [
'my_send_abandoned_cart_payload',
'my_send_abandoned_cart_payload_24h',
'my_send_abandoned_cart_payload_48h',
];
$crons = _get_cron_array();
foreach ( $hooks as $hook ) {
if ( ! empty( $crons[ $hook ] ) ) {
foreach ( $crons[ $hook ] as $ts => $jobs ) {
foreach ( $jobs as $job ) {
if ( isset($job['args'][0]) && $job['args'][0] === $sid ) {
wp_unschedule_event( $ts, $hook, [ $sid ] );
}
}
}
}
}
}
PHP
Now we need to build it:
The key things for me to prove could be done in flowmattic were:
- Firing on the Custom Action
- Extracting our cart-based product list
- Generating a short-duration (24hr) coupon in WooCommerce
- Sending our new flowmattic email designer email to the customer with the coupon and products listed
We already know that Flowmattic can handle delays and routers, so I didn’t need to work on adding those. I’ve already covered those in detail in other videos and articles.
The below image is the workflow I settled on:

In the below video which accompanies this article you can see how the various parts of the flowmattic flow come together.
If you know of a Free Plugin or an inbuilt hook in WooCommerce I couldn’t find that is suitable to replace The custom code please do let me know so I can update the article.