WooCommerce & FlowMattic – Abandon Cart

Tech Articles | May 3, 2025 | Automation, Blog, Coding, WooCommerce, Wordpress

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.

Support the Author

buy me a coffee
Really Useful Plugin Logo
Wpvideobank temp
Appoligies for any spelling and grammer issue. As a dyslexic i need to rely on tools for this they like me are not perfect but I do try my best