Skip to content

Trigv — Developer Guide

How to send your own notifications, shape or veto dispatches, register custom Triggers, and work on the plugin itself.

All hooks use the trigv_ prefix. The PHP namespace is Trigv\.

  • Trigger — a WordPress hook the admin has chosen to watch.
  • Notification — the payload sent to Trigv (channel, title, …).
  • Dispatch — turning a fired Trigger (or a trigv_send call) into a Notification and sending it asynchronously via Action Scheduler.
  • Token — a {placeholder} in a title/description template, resolved from a Trigger’s context.

See CONTEXT.md for the full glossary.

Fire the trigv_send action from anywhere. The Notification is queued and sent in the background (with retries), so it never blocks the request.

do_action( 'trigv_send', array(
'channel' => 'general',
'title' => 'Deploy complete',
'description' => 'Build #123 shipped to production.',
'level' => 'success', // info | success | warning | error
) );

Full set of arguments:

do_action( 'trigv_send', array(
'channel' => 'ops',
'title' => 'Payment webhook failed',
'description' => 'Stripe returned 500 for charge ch_123.',
'level' => 'error',
'event_type' => 'stripe.webhook.failed', // machine-readable type
'delivery_urgency' => 'time_sensitive', // break through iOS Focus
'image_url' => 'https://example.com/snapshot.jpg',
'idempotency_key' => 'stripe-ch_123-failed', // dedupe retries
) );

From a cron job:

add_action( 'my_nightly_report', function () {
$sales = my_get_todays_sales();
do_action( 'trigv_send', array(
'channel' => 'reports',
'title' => sprintf( 'Nightly report: %d orders', $sales['count'] ),
'description' => sprintf( 'Revenue: %s', $sales['total'] ),
'level' => 'info',
'event_type' => 'report.nightly',
) );
} );
KeyRequiredNotes
channelYesTrigv channel slug.
titleYesMax 255 chars.
descriptionNoMax 1000 chars.
levelNoinfo (default), success, warning, error. Styling only.
event_typeNoMachine-readable, e.g. deploy.complete.
delivery_urgencyNostandard (default) or time_sensitive.
image_urlNoHTTPS image passed through to devices.
idempotency_keyNoStable key so retries don’t double-count. Auto-generated if omitted.

Shaping every dispatch — trigv_dispatch_args

Section titled “Shaping every dispatch — trigv_dispatch_args”

Applied to every Notification (from a Trigger or from trigv_send) right before it is queued. Return the (possibly modified) args.

Route all errors to a dedicated channel and make them urgent:

add_filter( 'trigv_dispatch_args', function ( array $args, array $context ) {
if ( 'error' === ( $args['level'] ?? '' ) ) {
$args['channel'] = 'critical';
$args['delivery_urgency'] = 'time_sensitive';
}
return $args;
}, 10, 2 );

$context tells you where the dispatch came from:

add_filter( 'trigv_dispatch_args', function ( array $args, array $context ) {
// $context['source'] => Trigger id (e.g. 'post_published') or 'manual'
// $context['trigger'] => human label
$args['title'] = '[' . get_bloginfo( 'name' ) . '] ' . $args['title'];
return $args;
}, 10, 2 );

Return false (or a WP_Error) to suppress a Notification.

Never notify from staging/local:

add_filter( 'trigv_pre_dispatch', function ( $send, array $args ) {
if ( 'production' !== wp_get_environment_type() ) {
return false;
}
return $send;
}, 10, 2 );

Mute a specific event type overnight:

add_filter( 'trigv_pre_dispatch', function ( $send, array $args ) {
$hour = (int) current_time( 'G' );
if ( 'comment.created' === ( $args['event_type'] ?? '' ) && ( $hour >= 23 || $hour < 7 ) ) {
return false;
}
return $send;
}, 10, 2 );

Controlling the client IP — trigv_client_ip

Section titled “Controlling the client IP — trigv_client_ip”

Used by the “Failed login” Trigger’s {ip} token. Default is REMOTE_ADDR.

Behind Cloudflare (use the real visitor IP):

add_filter( 'trigv_client_ip', function ( string $ip ) {
if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
}
return $ip;
} );

Privacy-first (store a hashed IP instead of the raw address):

add_filter( 'trigv_client_ip', fn( string $ip ) => hash( 'sha256', $ip . wp_salt() ) );

Widening “Post published” — trigv_post_published_types

Section titled “Widening “Post published” — trigv_post_published_types”

By default the “Post published” Trigger fires only for post. Pages have their own “Page published” Trigger; add any other public post types here:

add_filter( 'trigv_post_published_types', function ( array $types ) {
$types[] = 'page';
$types[] = 'product';
return $types;
} );

Registering custom Triggers — trigv_triggers

Section titled “Registering custom Triggers — trigv_triggers”

Push Trigv\Trigger instances onto the catalog. Each Trigger declares the WordPress hook to watch and a resolver that turns the hook’s arguments into a Token map (or returns null to skip that firing). The admin can then enable it and set its channel/level/template like any built-in Trigger.

Example: notify when a user is promoted to administrator

Section titled “Example: notify when a user is promoted to administrator”
add_filter( 'trigv_triggers', function ( array $triggers ) {
if ( ! class_exists( \Trigv\Trigger::class ) ) {
return $triggers;
}
$triggers[] = new \Trigv\Trigger(
id: 'user_became_admin',
label: 'User promoted to administrator',
group: 'Security',
event_type: 'user.role.admin',
default_level: 'warning',
default_title: 'New administrator: {user_login}',
default_description: '{display_name} ({user_email})',
tokens: array(
'user_login' => 'Username',
'display_name' => 'Display name',
'user_email' => 'Email address',
),
hook: 'set_user_role',
priority: 10,
accepted_args: 3, // $user_id, $role, $old_roles
resolver: static function ( $user_id, $role, $old_roles = array() ): ?array {
// Only fire when someone *becomes* an admin.
if ( 'administrator' !== $role || in_array( 'administrator', (array) $old_roles, true ) ) {
return null;
}
$user = get_userdata( (int) $user_id );
return $user ? array(
'user_login' => $user->user_login,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
) : null;
},
);
return $triggers;
} );

A complete Add-on lives in examples/woocommerce-trigv-addon:

add_filter( 'trigv_triggers', function ( array $triggers ) {
if ( ! class_exists( \Trigv\Trigger::class ) ) {
return $triggers;
}
$triggers[] = new \Trigv\Trigger(
id: 'woo_order_completed',
label: 'WooCommerce order completed',
group: 'WooCommerce',
event_type: 'woo.order.completed',
default_level: 'success',
default_title: 'Order #{order_id} completed',
default_description: '{total} from {customer}',
tokens: array(
'order_id' => 'Order number',
'total' => 'Order total',
'customer' => 'Customer name',
),
hook: 'woocommerce_order_status_completed',
priority: 10,
accepted_args: 2, // $order_id, $order
resolver: static function ( $order_id, $order = null ): ?array {
$order = $order instanceof \WC_Order ? $order : wc_get_order( (int) $order_id );
return $order ? array(
'order_id' => (string) $order->get_id(),
'total' => html_entity_decode( wp_strip_all_tags( $order->get_formatted_order_total() ) ),
'customer' => $order->get_formatted_billing_full_name(),
) : null;
},
);
return $triggers;
} );
ArgumentTypeNotes
idstringUnique slug (also the config key).
labelstringShown in the admin.
groupstringUI grouping (e.g. Content, Security).
event_typestringSent to Trigv as event_type.
default_levelstringinfo/success/warning/error.
default_titlestringTemplate with {tokens}.
default_descriptionstringTemplate with {tokens}.
tokensarraytoken => label map shown as hints.
hookstringWordPress hook to watch.
priorityintHook priority.
accepted_argsintNumber of args the hook passes.
resolverClosurefn( ...$args ): ?array — Token map, or null to skip.

Titles and descriptions are templates. Each {token} is replaced by the value the resolver returned for that key; unknown tokens are left untouched. Admins can override a Trigger’s title/description in the UI, and the placeholders shown are exactly the Trigger’s declared tokens.

The admin app talks to these routes under trigv/v1. All require manage_options and a valid wp_rest nonce.

MethodRoutePurpose
GET / POST/trigv/v1/settingsRead / update connection (API key never returned).
GET / POST/trigv/v1/triggersRead catalog + config / save per-Trigger config.
POST/trigv/v1/testSend a test Notification.
GET / DELETE/trigv/v1/logRead / clear the recent-dispatch log.
Terminal window
composer install # PHP deps (Action Scheduler, GitHub updater)
npm install # JS deps
npm run start # Watch/rebuild the admin app
npm run build # Production build
composer test # PHPUnit (Brain Monkey, no WordPress required)
npm test # Vitest (jsdom)
npm run lint:js # ESLint (src only)
ClassResponsibility
PluginBootstrap and wiring
SettingsConnection config (API key, defaults)
TriggerCatalog / TriggerRegistry of available Triggers
TriggerConfigPer-Trigger configuration (enabled, channel, level, templates)
NotificationImmutable notification value object — build, validate, payload
DispatcherEnqueue, send, and retry Notifications
TrigvClientHTTP transport for the Trigv API
RestControllertrigv/v1 REST routes for the admin app
AdminPageMenu + React app mount
LogRecent-dispatch ring buffer

See docs/adr for architecture decisions.