Skip to content

Hooks & filters

The plugin's public surface is intentionally small: one action for writing entries, and four filters for shaping behavior. Everything is prefixed order_trail.

Action: order_trail

Write a custom audit note through the plugin's pipeline (prefix applied, no actor suffix — compose your own text):

php
do_action( 'order_trail', $order_id, 'Contract #42 attached' );

The message may contain safe HTML such as links. If the plugin is inactive the action is simply a no-op, so calling code needs no class_exists guard (though it may want a fallback if the note is essential).

Filter: order_trail_note_prefix

Prefix prepended to every note the plugin writes. Default: '' (no prefix).

php
add_filter( 'order_trail_note_prefix', function () {
    return '[Audit] ';
} );

Filter: order_trail_ignored_props

Order fields excluded from change logging. Receives the default list and the order being saved:

php
add_filter( 'order_trail_ignored_props', function ( $props, $order ) {
    $props[] = 'transaction_id';
    return $props;
}, 10, 2 );

Removing entries from the list works too — for example, dropping 'status' would add a field-diff line for status changes (normally left entirely to WooCommerce's own note).

Filter: order_trail_watched_meta

Order meta keys to diff and log on save. Empty by default — meta logging is strictly opt-in, because meta payloads are unbounded and most keys are machine state, not audit material. Entries may be plain keys (shown verbatim in the note) or key => label pairs (the label is shown instead):

php
add_filter( 'order_trail_watched_meta', function ( $keys, $order ) {
    $keys[] = '_delivery_date';        // note shows the key verbatim
    $keys['_po_number'] = 'PO Number'; // note shows the readable label
    return $keys;
}, 10, 2 );
Order updated - _delivery_date: 2026-08-01 -> 2026-08-15 [by mike]
Order updated - PO Number: PO-1001 -> PO-2002 [by mike]

Only changes made through the order object and saved with it are seen ($order->update_meta_data() + $order->save(), which is also what admin and REST edits use). Low-level writes that bypass the order CRUD — direct update_post_meta() calls or raw SQL — are not caught.

Filter: order_trail_email_notes

Master switch for the built-in order-email notes. Default true. Return false when another email logger writes richer notes, to avoid duplicates:

php
add_filter( 'order_trail_email_notes', '__return_false' );

Filter: order_trail_email_note

Adjust the email note text before it is written. Receives the message, order, WooCommerce email ID, and the WC_Email instance:

php
add_filter( 'order_trail_email_note', function ( $message, $order, $email_id, $email ) {
    return $message . ' (' . $email_id . ')';
}, 10, 4 );

Integrating a custom email logger

A site that stores full email copies (in a custom post type, for example) can take over email notes entirely while keeping them in the audit trail — disable the built-in notes and write richer ones through the action:

php
// In the email logger, once the copy is stored:
add_filter( 'order_trail_email_notes', '__return_false' );

$note = sprintf(
    'Email sent to %s: "%s" <a href="%s">View email</a>',
    esc_html( $recipient ),
    esc_html( $subject ),
    esc_url( $view_url )
);
do_action( 'order_trail', $order_id, $note );

The dependency points from the site to the plugin — the plugin never needs to know the site's code exists.

PHP API

Two public methods complement the hooks:

MethodPurpose
Order_Trail::reset_request_state()Clear per-request suppression sets in long-running processes (queue workers, test suites).
Order_Trail::log_email_sent( $return, $email_id, $email )The woocommerce_email_sent handler, callable directly for custom mailers that mimic WC_Email.

Released under the GPL-2.0-or-later License.