Where WooCommerce admin reports actually get read

Three Surfaces: an isometric sorting console sends report cards to a dashboard panel, an envelope, and an orders shelf.

Most people who run a WooCommerce store spend their admin day on two screens: Orders and Products. Build a WooCommerce admin report as its own page under your own menu slug and you have mostly decided it will be opened once, on demo day. I stopped treating that as a client problem a while ago.

Hayden Kastens wrote about the same failure in data science: stakeholders requested a model, his team built it, and nobody used it. He names three causes: the model was a mystery, it arrived too late, and it didn’t live where the work happens. The first two are communication problems. The third is a build decision, and that’s the one worth arguing about.

Where WooCommerce admin reports live

You have three realistic surfaces: a standalone admin page, a scheduled email, or a screen the manager already has open. I’ve shipped all three and they behave differently a month after launch.

The admin page, via add_menu_page, is the easiest to build and the fastest to die. It gives you full control of layout and zero claim on anyone’s attention. A store manager’s loop is open admin, click Orders, process orders, close. Nothing in that loop points at your page. The data can be correct and it still won’t get read, because you made reading it a separate task.

A scheduled email pushes instead of pulling, which matters for owners who aren’t in the admin every day. wp_schedule_event handles the timing, but WP-Cron fires on page loads, so on a quiet store a Monday morning email can leave hours late. On steady traffic, or with a real cron hitting wp-cron.php, it’s dependable. Email flattens nuance too: one glance, no sorting, no drilldown.

The third option is the number inside a screen the person already uses: a column on the orders list, a metabox on the single order view, a column on Products. It’s the most work of the three, mostly because of HPOS, and the only one that rides along with a task the person was doing anyway. Kastens’ clinicians started using predictions once they appeared inside Epic, next to the patient chart. The WooCommerce equivalent of the chart is the orders list.

The orders list column and the HPOS detail

Two decisions make this version hold up. Compute the score ahead of time and store it as order meta, so the column callback is a meta read rather than a query. And use the current hooks, because the orders list table changed with High-Performance Order Storage and the legacy manage_edit-shop_order_columns filter doesn’t run when HPOS is authoritative:

add_filter( 'manage_woocommerce_page_wc-orders_columns', 'bbioon_add_risk_column' );
add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'bbioon_render_risk_column', 10, 2 );

function bbioon_add_risk_column( array $columns ) : array {
    $columns['bbioon_return_risk'] = __( 'Return risk', 'bbioon' );
    return $columns;
}

function bbioon_render_risk_column( string $column, WC_Order $order ) : void {
    if ( 'bbioon_return_risk' !== $column ) {
        return;
    }
    $score = $order->get_meta( '_bbioon_return_risk' );
    echo $score ? esc_html( $score . '%' ) : '-';
}

Calculate _bbioon_return_risk in a nightly event or on woocommerce_order_status_changed, never inside the column callback. If this ships as a plugin, declare compatibility on before_woocommerce_init with FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ), as the HPOS recipe book documents. Name the column the way the manager talks, “Return risk” rather than “p_refund_30d”. If the number needs justifying, the single order screen has room for the reasoning; the list table has room for a label. Defining the metric in the owner’s language first is the same argument as starting analytics by defining revenue.

Which surface for which store

Small store, owner rarely in the admin: the email wins, because a handful of numbers once a week is the whole need. If staff process orders daily, embed it. Email gets ignored in packing season, and a column gets seen whether or not anyone chose to look. For an agency maintaining many client sites, embedding wins for a blunter reason: you won’t be there every week to remind anyone the page exists, and a column shows up in screenshots when the client asks what they’re paying for.

The standalone page still earns its keep as a workbench for exploration and one-off analysis. It’s the wrong delivery surface for anything you want used weekly. If a client insists on a dashboard anyway, what separates used from unused is whether it answers one named question, which is close to what I argued about real-time dashboards people can actually read.

If you have a report or a score sitting on a page nobody opens, I can move it into the orders or products screen, meta and hooks included, HPOS-safe. Once the number itself is agreed, most of these jobs are a day or two.

Before shipping a column, open the orders screen with the date filter set to a month of data on the busiest store you can access. If rendering adds anything you can feel, precompute into meta instead of shipping it. And check which storage the store uses before writing the hooks: a store still on legacy posts storage fires the old hooks and not the new ones, and OrderUtil::custom_orders_table_usage_is_enabled() tells you which world you’re in. I can’t speak for stores doing tens of thousands of orders a month; I haven’t pushed a custom column that far.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.

Leave a Comment