Store Projects

Project: AI Product Recommendations

⏱ 16 min

Worth being precise about what WooCommerce already does here, because it’s easy to assume more than it actually offers. WooCommerce’s built-in “related products” feature is similarity-based, not behavior-based: two products are considered related if they share a category or tag, full stop. There’s no native, built-in “customers who bought X also bought Y” feature computed from actual purchase behavior. Third-party services (Nosto, Barilliance, LimeSpot, and similar) fill that gap commercially, usually by sending customer data to their own servers. This lesson builds a much simpler, self-hosted version of the same idea, grounded in your own order data.

What you'll learn in this lesson
Why native "related products" isn't behavioral
Category/tag similarity versus actual co-purchase patterns.
Computing co-purchase counts from real order line items
A lightweight collaborative-filtering approach using wc_get_orders().
Using wp_ai_client_prompt() for wording, not for choosing products
The model phrases a recommendation, your PHP already decided which products.
Caching, because this is expensive to compute live
A transient-backed approach so this doesn't recompute on every request.
Prerequisites

WooCommerce with meaningful order history involving multiple products per order, a handful of single-item test orders won’t produce useful co-purchase signal. No specific WooCommerce version requirement, this lesson uses wc_get_orders() directly rather than the 10.9 abilities.

Step 1: Computing co-purchase counts

// File: my-plugin.php
function my_plugin_compute_co_purchases( $product_id, $limit = 200 ) {
	$cache_key = 'my_plugin_co_purchase_' . $product_id;
	$cached    = get_transient( $cache_key );
	if ( false !== $cached ) {
		return $cached;
	}

	$orders   = wc_get_orders( array( 'status' => array( 'completed', 'processing' ), 'limit' => $limit ) );
	$co_counts = array();

	foreach ( $orders as $order ) {
		$product_ids_in_order = array();
		foreach ( $order->get_items() as $item ) {
			$product_ids_in_order[] = $item->get_product_id();
		}

		if ( ! in_array( $product_id, $product_ids_in_order, true ) ) {
			continue;
		}

		foreach ( $product_ids_in_order as $other_id ) {
			if ( $other_id === $product_id ) {
				continue;
			}
			$co_counts[ $other_id ] = ( $co_counts[ $other_id ] ?? 0 ) + 1;
		}
	}

	arsort( $co_counts );
	$top = array_slice( $co_counts, 0, 5, true );

	set_transient( $cache_key, $top, 6 * HOUR_IN_SECONDS );
	return $top;
}

This is a deliberately simple approach, order-level co-occurrence counts, not a weighted or normalized collaborative-filtering model. It’s honest about that simplicity rather than dressing it up as something more sophisticated than it is.

Step 2: Registering the ability

// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'my-plugin/recommend-related-products',
		array(
			'label'         => __( 'Recommend related products', 'my-plugin' ),
			'description'   => __( 'Suggests products frequently purchased alongside a given product, based on real order history, then phrases the suggestion in plain language.', 'my-plugin' ),
			'category'      => 'store-inventory',
			'input_schema'  => array(
				'type'       => 'object',
				'properties' => array(
					'product_id' => array( 'type' => 'integer' ),
				),
				'required'   => array( 'product_id' ),
			),
			'output_schema' => array(
				'type'       => 'object',
				'properties' => array(
					'candidates' => array( 'type' => 'array' ),
					'blurb'      => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => '__return_true',
			'execute_callback' => function ( $input ) {
				$co_purchases = my_plugin_compute_co_purchases( $input['product_id'] );
				if ( empty( $co_purchases ) ) {
					return array( 'candidates' => array(), 'blurb' => '' );
				}

				$candidates = array();
				foreach ( $co_purchases as $id => $count ) {
					$p = wc_get_product( $id );
					if ( $p ) {
						$candidates[] = array( 'id' => $id, 'name' => $p->get_name(), 'co_purchase_count' => $count );
					}
				}

				$prompt = sprintf(
					"Write a one-sentence 'customers also bought' suggestion using ONLY these products, " .
					"do not add any product not listed: %s",
					wp_json_encode( $candidates )
				);

				$blurb = wp_ai_client_prompt( $prompt )->generate_text();

				return array( 'candidates' => $candidates, 'blurb' => trim( $blurb ) );
			},
			'meta' => array(
				'annotations' => array( 'readonly' => true ),
				'mcp'         => array( 'public' => true ),
			),
		)
	);
} );

Note the order of operations: $candidates is fully computed by my_plugin_compute_co_purchases(), real PHP, real order data, before the model ever sees a single product name. The AI Client SDK call in this ability only phrases a sentence, it has no ability to introduce a product that wasn’t in the candidate list.

Step 3: Why caching matters here specifically

wc_get_orders() over hundreds of orders on every single product-page view would be expensive to run live, so the six-hour transient cache in Step 1 is not optional polish, it’s the difference between this being viable on a real store versus a noticeable slowdown on every page load. Widen or shorten the cache window based on how often your order volume meaningfully shifts the top candidates.

Test it

wp-env run cli wp eval '
$ability = wp_get_ability( "my-plugin/recommend-related-products" );
$result  = $ability->execute( array( "product_id" => 42 ) );
print_r( $result["candidates"] );
echo $result["blurb"] . "\n";
'

Confirm every product named in the blurb also appears in candidates, if it doesn’t, the prompt constraint needs tightening.

Letting the model recommend a product from memory, not from candidates

If your prompt is too loosely worded (“suggest some related products” instead of “use ONLY these products”), a capable model may recall product names from earlier in the same conversation or session and slip one into the blurb that was never in your computed candidate list. Always phrase the instruction as a hard constraint, and treat any output that names something outside candidates as a prompt bug to fix, not something to work around downstream.

Recap

This recommendation ability computes real co-purchase counts from order line items, caches the expensive part, and uses wp_ai_client_prompt() purely to phrase a sentence around candidates your own PHP already selected. It’s a meaningfully simpler approach than WooCommerce’s native category/tag-based related products (which isn’t behavior-based at all) and a much lighter-weight one than dedicated commercial recommendation services, appropriate for stores that want something self-hosted and data-grounded rather than a full recommendation platform.

Resources & further reading

← Project: Natural-Language Store Manager Project: Customer-Query Handler →