Four actions mark when HappenBoard is safe to touch and when its data goes away. Two fire on every request, at the end of the free plugin’s bootstrap and at the end of Pro’s. Two fire only while WordPress deletes a plugin, and both are behind an opt-in constant.
If you are writing a companion plugin, happenboard_loaded is the one hook you need. It replaces plugins_loaded plus a class_exists() guard, and it is where Pro itself boots.
happenboard_loaded
Fires on the last line of HappenBoard\Plugin::boot(), which itself runs on plugins_loaded at the default priority. Every post type, taxonomy, meta field, block, REST route and admin screen the free plugin contributes is registered by then, so this is the point where HappenBoard’s classes and filters are safe to touch. Every integration should start here rather than on plugins_loaded.
Signature
do_action( 'happenboard_loaded' )
Example
add_action(
'happenboard_loaded',
static function (): void {
// Reaching this line proves HappenBoard is installed, active and
// fully booted. The action does not exist otherwise, so there is
// nothing to guard with class_exists() and no "please install"
// notice to plumb: your integration simply stays dormant.
// Safe now. Every HappenBoard class is autoloadable and every post
// type, taxonomy, meta field, block and REST route is registered,
// so filters can be attached without an existence check.
add_filter( 'happenboard_screen_context', 'my_companion_announce' );
add_filter( 'happenboard_admin_submenus', 'my_companion_submenus' );
// We are still inside plugins_loaded, before init. Anything that
// belongs on init (post meta, taxonomies, textdomain, cron
// scheduling) is deferred, not called here.
add_action( 'init', 'my_companion_register_meta', 11 );
add_action( 'rest_api_init', 'my_companion_register_routes' );
// Optional: gate on a minimum version of the free plugin.
if ( version_compare( HAPPENBOARD_VERSION, '2.0', '<' ) ) {
return;
}
my_companion_register_new_stuff();
}
);
Notes
- Use it instead of
plugins_loaded. Onplugins_loadedyour callback can run before or after HappenBoard’s own boot depending on plugin load order and priority, so referencing a HappenBoard class from there is a coin flip. This action fires afterboot()has wired everything. - It doubles as the presence check. The action only exists when the plugin is loaded, so a callback that never runs is your graceful degradation path: no
class_exists()guard, no admin notice plumbing. If you need a minimum version, testHAPPENBOARD_VERSIONinside the callback. - Pro boots from it.
happenboard-pro.phphooks this action at the default priority 10 and callsHappenBoard\Pro\Plugin::boot(). Hook at a priority above 10 if you must run after Pro’s services are wired, or usehappenboard_pro_loaded. - It fires during
plugins_loaded, beforeinit. Do not callregister_post_meta(),register_taxonomy(),load_plugin_textdomain()or anything translated directly in the callback: add aninitlistener from inside it. HappenBoard itself does this for its capability install, oninitat priority 1. - No arguments are passed, so the accepted-args argument of
add_action()is never needed. It fires on every request: front end, admin, REST and WP-CLI alike.
happenboard_pro_loaded
Fires on the last line of HappenBoard\Pro\Plugin::boot(), once every Pro service is wired: REST routes, Pro blocks, custom fields, ticketing, calendar sync, webhooks and the third-party integrations. Use it for add-ons that extend Pro features rather than free ones.
Signature
do_action( 'happenboard_pro_loaded' )
Example
add_action(
'happenboard_pro_loaded',
static function (): void {
// Reached only when HappenBoard AND HappenBoard Pro are both
// active: Pro boots from `happenboard_loaded`, and fires this at
// the end of its own boot(). So this is nested inside Free's hook
// and still runs during plugins_loaded.
my_addon_wire_pro_filters();
// Pro creates its tables and installs its capabilities on init
// (priorities 1 and 2), which is after this action. Any query
// against a Pro table has to wait.
add_action( 'init', 'my_addon_read_pro_tables', 5 );
}
);
Notes
- It is nested inside
happenboard_loaded: Pro’s main file hooks the free action at priority 10 and callsboot(), which ends with this one. So it also runs duringplugins_loaded, beforeinit, and it never fires when the free plugin is inactive. - This is the reliable Pro-is-active signal.
HAPPENBOARD_PRO_VERSIONis defined when the plugin file is merely loaded, which tells you nothing about whether the services were wired. This action does. - Pro’s database tables and capabilities are not ready yet.
boot()schedulesmaybe_install_capabilitiesoninitat priority 1 andmaybe_run_migratorat priority 2, both after this action. Defer any read of{prefix}happenboard_ticketstoinitpriority 3 or later. - Every filter Pro attaches in
boot()is already in place. Hooking the same filter here at the same priority puts you after Pro’s callback, which is usually what you want when you are adjusting a Pro-produced value. - No arguments are passed, so no accepted-args value is needed. Pro declares
Requires Plugins: happenboardand boots from the free action, so there is no scenario where this fires with HappenBoard missing.
happenboard_pro_uninstall
Fires while WordPress deletes HappenBoard Pro, at the very top of Pro’s per-blog cleanup routine and before Pro removes anything of its own. Add-ons hook it to drop their options, tables and capabilities in the same pass.
Signature
do_action( 'happenboard_pro_uninstall' )
Example
add_action(
'happenboard_pro_uninstall',
static function (): void {
global $wpdb;
delete_option( 'my_addon_settings' );
// Fires once per blog on multisite, inside switch_to_blog(), so
// $wpdb->prefix already points at the right site.
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_addon_pro_log" );
if ( function_exists( 'wp_roles' ) ) {
foreach ( wp_roles()->role_objects as $role ) {
$role->remove_cap( 'my_addon_manage' );
}
}
}
);
// Register it from your plugin's main file or from plugins_loaded.
// Registering it inside a `happenboard_pro_loaded` callback would never
// work: Pro is deactivated before WordPress runs its uninstall.php, so
// that action does not fire in the uninstall request.
Notes
- Nothing runs without the opt-in constant.
HappenBoard_Pro_Uninstallerreturns immediately from its constructor unlessHAPPENBOARD_PRO_UNINSTALLis defined and truthy, typically viadefine( 'HAPPENBOARD_PRO_UNINSTALL', true );inwp-config.php. Deleting Pro without it runsuninstall.php, but this action never fires and no data is removed. - Do not assume Pro cleaned up after itself.
uninstall.phpdrops{prefix}hap_tickets, but the table Pro actually creates is{prefix}happenboard_tickets(Database\Schema\TicketsTable::NAME), so the tickets table survives the uninstall untouched. The Stripe idempotency transients have the same problem: the script sweeps_transient_evp_stripe_evt_*whileWebhookHandlerwrites them under thehappenboard_stripe_evt_prefix. If your callback depends on Pro’s footprint being gone, it will be wrong. - Register the listener from your plugin’s main file or from
plugins_loaded, never from ahappenboard_pro_loadedcallback. Pro is deactivated before WordPress runs itsuninstall.php, sohappenboard_pro_loadeddoes not fire in that request and youradd_action()would never be reached. - On multisite the uninstaller loops every blog with
switch_to_blog()and calls the cleanup once per site, so your callback runs once per blog with$wpdbalready scoped correctly. Make it idempotent and never assume a single invocation. - It fires before Pro’s own deletions, so
happenboard_pro_*options are still readable at that point. Be aware the script then runs aLIKE 'happenboard_pro_%'sweep over the options table: any option of yours using that prefix is deleted whether you asked for it or not.
happenboard_uninstall
Fires while WordPress deletes the free HappenBoard plugin, at the very top of the per-blog cleanup and before HappenBoard removes any of its own data. Extensions hook it to delete their options, tables, cron events and capabilities in the same pass.
Signature
do_action( 'happenboard_uninstall' )
Example
add_action(
'happenboard_uninstall',
static function (): void {
global $wpdb;
// This runs before HappenBoard deletes anything of its own, so the
// events and their meta are still readable if you need them to
// decide what to remove.
delete_option( 'my_companion_settings' );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_companion_data" );
wp_clear_scheduled_hook( 'my_companion_cron' );
if ( function_exists( 'wp_roles' ) ) {
foreach ( wp_roles()->role_objects as $role ) {
$role->remove_cap( 'my_companion_manage' );
}
}
}
);
// Register it from your plugin's main file or from plugins_loaded, never
// from a `happenboard_loaded` callback: HappenBoard is deactivated before
// WordPress runs its uninstall.php, so `happenboard_loaded` does not fire
// in that request.
Notes
- Destructive cleanup is opt-in.
HappenBoard_Uninstallerreturns straight out of its constructor unlessHAPPENBOARD_UNINSTALLis defined and truthy, normally by addingdefine( 'HAPPENBOARD_UNINSTALL', true );towp-config.phpbefore deleting the plugin. Without it, every event, attendee and option stays in place and this action never fires. - Register the listener from your plugin’s main file or from
plugins_loaded, not from ahappenboard_loadedcallback. HappenBoard is deactivated before WordPress runs itsuninstall.php, sohappenboard_loadeddoes not fire in that request and your registration would never happen. - It runs first, before HappenBoard deletes anything. The
_happenboard_*post meta, thehboard_event/hboard_venue/hboard_organizerposts, the taxonomy terms, the options, the transients, the four custom tables and the capabilities are all removed after your callback returns, so you can still read HappenBoard data to decide what of yours to delete. - The option cleanup that follows is a
LIKEsweep onhappenboard_%andwidget_happenboard_%. Any option of yours sitting under those prefixes is deleted whether you wanted it or not, so pick your own prefix. - On multisite the uninstaller loops every blog with
switch_to_blog(), so your callback fires once per site with$wpdbpointed at that blog. Pro is a separate script with a separate constant, so this action says nothing about whether Pro was cleaned up.