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

Flowmattic Abandon Cart Woo Prcess

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:

Flowmattic Abandon Cart Woo Flow Output

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

Support my work
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