agentsclimarketplace

Wc hpos compatibility

Skill Lonsdale201/wp-agent-skills/woocommerce/wc-hpos-compatibility

Make WooCommerce order integrations compatible with High-Performance Order Storage. Covers compatibility declaration, order CRUD and queries, authoritative storage, sync mode, HPOS-aware admin list/meta-box hooks, cache-safe metadata access, migrations, and dual-mode testing. Use when a plugin reads or writes orders, queries order metadata, adds order admin UI, runs SQL, or declares `custom_order_tables` compatibility.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wc-hpos-compatibility

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.2 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it

WooCommerce HPOS compatibility

HPOS is the default order storage for new WooCommerce installs. An order ID is a WooCommerce entity ID, not a promise that a matching shop_order post or postmeta row exists.

Storage model

HPOS uses four primary tables:

wp_wc_orders
wp_wc_order_addresses
wp_wc_order_operational_data
wp_wc_orders_meta

Compatibility/synchronization mode can maintain data in both HPOS and legacy posts, and placeholder posts may exist. Neither is permission to read or write both stores directly. WooCommerce CRUD owns the authoritative store and synchronization.

Declare compatibility

Only declare after the plugin actually passes HPOS tests:

use Automattic\WooCommerce\Utilities\FeaturesUtil;

add_action( 'before_woocommerce_init', static function (): void {
    if ( class_exists( FeaturesUtil::class ) ) {
        FeaturesUtil::declare_compatibility( 'custom_order_tables', MYPLUGIN_FILE, true );
    }
} );

The third argument means compatible, not "enable HPOS". Do not declare true to hide an incompatibility warning while direct order post/meta code remains.

Read and write through CRUD

$order = wc_get_order( $order_id );

if ( $order instanceof WC_Order ) {
    $external_id = (string) $order->get_meta( '_myplugin_external_id' );

    $order->update_meta_data( '_myplugin_external_id', $new_external_id );
    $order->save();
}

Use object getters/setters for first-class properties such as status, billing address, transaction ID, dates, totals, currency, customer, and payment method. Use order meta only for extension-owned data.

Do not use these for order data:

get_post_meta( $order_id, '_billing_email', true );
update_post_meta( $order_id, '_myplugin_external_id', $value );
get_post( $order_id );
WP_Query( array( 'post_type' => 'shop_order' ) );

Query orders

$result = wc_get_orders( array(
    'status'     => array( 'processing', 'completed' ),
    'meta_query' => array(
        array(
            'key'     => '_myplugin_exported',
            'compare' => 'NOT EXISTS',
        ),
    ),
    'limit'      => 100,
    'page'       => 1,
    'paginate'   => true,
    'return'     => 'objects',
) );

foreach ( $result->orders as $order ) {
    // WC_Order objects from the active data store.
}

Use documented wc_get_orders()/WC_Order_Query fields. Raw SQL against either legacy or HPOS tables couples code to one backend, misses cache invalidation, and often mishandles refunds or date/status formats.

For large jobs, paginate and process asynchronously. Do not request every order in one frontend/admin request.

Runtime mode checks

Most code should not branch: CRUD is mode-neutral. Branch only for storage-specific admin hooks, diagnostics, or migration tools.

use Automattic\WooCommerce\Utilities\OrderUtil;

$hpos_active = class_exists( OrderUtil::class )
    && OrderUtil::custom_orders_table_usage_is_enabled();

OrderUtil::get_order_admin_screen() returns the active order edit/list screen ID for admin screen checks and meta boxes. It is admin-only and throws outside admin context; never call it from frontend, REST, cron, or WP-CLI code. OrderUtil::init_theorder_object() can normalize the post-or-order object passed into legacy-compatible meta-box code.

Do not infer active storage from table existence or a class name: both stores can exist during synchronization.

Order list columns

The hooks and callback arguments differ:

ModeColumns filterRender actionSecond render argument
HPOSmanage_woocommerce_page_wc-orders_columnsmanage_woocommerce_page_wc-orders_custom_columnWC_Order
Legacymanage_edit-shop_order_columnsmanage_shop_order_posts_custom_columnorder/post ID
add_action( 'admin_init', static function (): void {
    if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
        add_filter( 'manage_woocommerce_page_wc-orders_columns', 'myplugin_order_columns' );
        add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'myplugin_hpos_column', 10, 2 );
    } else {
        add_filter( 'manage_edit-shop_order_columns', 'myplugin_order_columns' );
        add_action( 'manage_shop_order_posts_custom_column', 'myplugin_legacy_column', 10, 2 );
    }
} );

function myplugin_order_columns( array $columns ): array {
    $columns['myplugin_ref'] = __( 'External reference', 'myplugin' );
    return $columns;
}

function myplugin_hpos_column( string $column, $order ): void {
    if ( 'myplugin_ref' === $column && $order instanceof WC_Order ) {
        echo esc_html( (string) $order->get_meta( '_myplugin_external_id' ) );
    }
}

function myplugin_legacy_column( string $column, int $order_id ): void {
    myplugin_hpos_column( $column, wc_get_order( $order_id ) );
}

Do not construct legacy hook names from the HPOS screen ID; their naming schemes differ.

Meta boxes

add_action( 'add_meta_boxes', static function (): void {
    $screen = OrderUtil::custom_orders_table_usage_is_enabled()
        ? OrderUtil::get_order_admin_screen()
        : 'shop_order';

    add_meta_box( 'myplugin-order', __( 'Integration', 'myplugin' ), 'myplugin_render_order_box', $screen, 'side' );
} );

Render callbacks should normalize their input to WC_Order, then use a nonce, capability check, CRUD setter/meta update, and $order->save() for writes.

Synchronization mode

Synchronization is transitional compatibility, not a second public write API.

  • Reads come from the configured authoritative store.
  • CRUD writes can be queued/synchronized to the backup store.
  • Sync-on-read is disabled by default in current WooCommerce.
  • Direct writes can diverge the two stores and are invisible to Woo caches.
  • Never assume a backup row is immediately present after a CRUD write.

Use WooCommerce's scheduled/CLI synchronization tools for diagnostics and migrations rather than custom table-copy SQL.

Test matrix

Before declaring compatibility, test at least:

  1. HPOS enabled, compatibility mode off.
  2. Legacy storage authoritative.
  3. HPOS with synchronization enabled.
  4. Order CRUD, metadata, refunds, queries, admin list columns, meta boxes, bulk actions, REST/webhooks, and uninstall/migration paths.

Run tests on a new order and an existing migrated order. Check that no code reads shop_order posts or order postmeta as the source of truth.

Audit search

rg -n "shop_order|WP_Query|get_posts|get_post_meta|update_post_meta|delete_post_meta|wp_postmeta|wc_orders" src

Every hit is not automatically wrong, but every order-related hit requires review.

Critical rules

  • Declare custom_order_tables compatibility only after dual-mode testing.
  • Treat WC_Order CRUD and wc_get_orders() as the public contract.
  • Never write both HPOS and legacy stores yourself.
  • Branch only where Woo admin hook contracts genuinely differ.
  • Keep long migrations and backfills idempotent and asynchronous.
  • Products, coupons, and variations are not moved by HPOS; do not over-apply order rules to them.

References

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.