These hooks act on events and their dates. Use them to follow the rebuild of a recurring event’s dates, clean up when an event is deleted for good or group translations. Others change the Event schema, the event addresses and what HappenBoard Pro pushes to Google Calendar and Outlook.
For the editor’s side, see recurring events and multilingual sites. New to hooks? Start with Extending HappenBoard with hooks.
How an event’s dates are stored
Each event is a post. Its dates live in HappenBoard’s own table: one row for a one-off event, one row per date for a recurring one. Each row has a sequence number (0 for the first date). The rows are rebuilt from the event’s settings, so never edit them directly.
On every save:
happenboard_occurrences_syncingfires while the old rows are still there.- The old rows are deleted, then the new ones are written.
happenboard_occurrences_changedfires after each step.
When an event is deleted for good (moving it to the trash only removes its rows):
- Its rows are deleted and
happenboard_occurrences_changedfires. happenboard_event_deletingfires. The post, its meta and its attendees can still be read.- Each attendee is deleted and fires
happenboard_attendee_deleted.
Translated events
With WPML or Polylang, each translation of an event is its own post, with its own dates and its own attendees. happenboard_public_occurrence_clauses keeps the public lists to the current language. The translations share their seats: every translation’s registrations count against the capacity of the default language’s translation. happenboard_translation_group decides which posts form that group.
happenboard_occurrences_syncing
Fires at the start of every rebuild of an event’s dates, while its rows still hold the old dates. Use it to note what an edit is about to change, then compare once the request is over.
Signature
do_action( 'happenboard_occurrences_syncing', int $event_id, bool $announce )
Parameters
-
$event_idint - The event whose dates are about to be rebuilt.
-
$announcebool truewhen the rebuild comes from an edit (a save, a date skipped or moved).falsewhen it only re-derives the rows: after a plugin update or a change of site time zone.
Example
add_action(
'happenboard_occurrences_syncing',
function ( int $event_id, bool $announce ): void {
global $wpdb;
if ( ! $announce ) {
return;
}
// The rows still hold the dates as they were before the edit.
$GLOBALS['my_old_dates'][ $event_id ] = $wpdb->get_col(
$wpdb->prepare(
"SELECT start_utc FROM {$wpdb->prefix}happenboard_occurrences WHERE event_id = %d AND is_cancelled = 0",
$event_id
)
);
},
10,
2
);
Notes
- Two arguments: register with
10, 2to receive$announce. - One request can rebuild an event’s dates more than once, so compare at the end of the request (on
shutdown). HappenBoard does the same at priority 10: it notes each attendee’s date, then emails those whose date was cancelled or moved. - It does not fire when an event is trashed or deleted for good: those only remove the rows, which fires
happenboard_occurrences_changed.
happenboard_occurrences_changed
Fires after an event’s date rows were written or removed: on a save, the trash, a permanent deletion, an import, WP-CLI or the daily extension of a long series. Use it to clear anything you cache from an event’s dates.
Signature
do_action( 'happenboard_occurrences_changed', int $event_id )
Parameters
-
$event_idint - The event whose rows changed.
Example
add_action(
'happenboard_occurrences_changed',
function ( int $event_id ): void {
// Our cached list of the next dates is now out of date.
delete_transient( 'my_plugin_next_dates_' . $event_id );
}
);
Notes
- One argument.
- A save fires it twice: once when the old rows are deleted, once when the new ones are written. During the first call the event has no rows, so do not treat what you read there as final.
- Moving an event to the trash removes its rows until it is restored. Deleting it for good removes them before
happenboard_event_deletingfires. - HappenBoard clears its cached iCal feed at priority 10 and, at priority 20, gives a date to the attendees of a recurring event who have none (
happenboard_attendee_dates_assigned). Keep your callback cheap: an import or a bulk edit fires it for every event.
happenboard_public_occurrence_clauses
Adds SQL to every public read of event dates: the Calendar, Upcoming Events, Events List, Events Grid and Event Query Loop blocks and the site iCal feed. Use it to keep some events out of the public lists, as HappenBoard does with WPML and Polylang to show one language.
Signature
apply_filters( 'happenboard_public_occurrence_clauses', array{join:string,where:string} $clauses, string $event_column, string $fallback )
Parameters
-
$clausesarray{join:string,where:string} - SQL fragments, both empty by default.
joinis added after the join on the posts table (aliasp; the dates table iso),whereis ANDed with the rest. Both must be fully prepared. -
$event_columnstring - Column holding the event ID. Always
'o.event_id'today. -
$fallbackstring - What to show when no language is current:
''for every language,'default'for the default language only (the site iCal feed).
Return value
array{join:string,where:string}
The fragments to apply. A return that is not an array, or a fragment that is not a string, adds nothing.
Example
add_filter(
'happenboard_public_occurrence_clauses',
function ( $clauses, $event_column ) {
global $wpdb;
// Keep members-only events out of every public list.
$condition = $wpdb->prepare(
"NOT EXISTS ( SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = {$event_column} AND pm.meta_key = %s )",
'_my_members_only'
);
$clauses['where'] = '' === $clauses['where']
? $condition
: '( ' . $clauses['where'] . ' ) AND ' . $condition;
return $clauses;
},
20,
2
);
Notes
- Two or three arguments: register with
10, 2for the column,10, 3for the language fallback. - Add to the incoming fragments. With WPML or Polylang active, HappenBoard adds a language JOIN at priority 10: returning fresh strings drops it, and every translation shows up again.
- Prepare every value with
$wpdb->prepare(). HappenBoard doubles each literal%for a secondprepare()pass, so do not double them yourself. - Only reads limited to published events run it. The admin Calendar, the date an event page shows and the AI abilities skip it. The iCal feed cache is keyed on the result, so return the same SQL for the same request context.
happenboard_translation_group
Filters the translations of an event: the posts whose registrations share its seats and its attendee list. HappenBoard fills it from WPML or Polylang; use it for another multilingual plugin that keeps each translation as its own post.
Signature
apply_filters( 'happenboard_translation_group', array{ids:array<int,int>,original:int,languages:array<int,string>} $group, int $event_id )
Parameters
-
$grouparray{ids:array<int,int>,original:int,languages:array<int,string>} ids: every translation, the event included, lowest ID first.original: the translation that holds the shared capacity (the default language’s, else the source, else the lowest ID).languages: each translation’s language code (fr), keyed by post ID, shown in the attendee list and export. Without translations: the event alone, as its own original.-
$event_idint - The event asked about.
Return value
array{ids:array<int,int>,original:int,languages:array<int,string>}
The group. HappenBoard always adds the event to ids, uses the lowest ID when original is not a member, and drops the languages of non-members. A return that is not an array leaves the event alone.
Example
add_filter(
'happenboard_translation_group',
function ( $group, $event_id ) {
// Our own plugin links each copy to its original with post meta.
$original = (int) get_post_meta( $event_id, '_my_translation_of', true ) ?: $event_id;
$copies = array_map( 'intval', (array) get_post_meta( $original, '_my_translations', true ) );
if ( array() === array_filter( $copies ) ) {
return $group;
}
$group['ids'] = array_merge( array( $original ), $copies );
$group['original'] = $original;
foreach ( $group['ids'] as $id ) {
$group['languages'][ $id ] = (string) get_post_meta( $id, '_my_language', true );
}
return $group;
},
10,
2
);
Notes
- Two arguments: register with
10, 2. - The group is read once per request and remembered for each member, so return the same group whichever member is asked about.
- The original’s capacity applies to the whole group, every translation’s registrations count against it, and one email registers once across the group. HappenBoard Pro also sells the group’s tickets on every translation and counts their check-ins together.
- The remembered groups are cleared when an event is saved or when WPML or Polylang link translations. If your plugin links them another way, call
\HappenBoard\Integration\Multilingual\TranslationGroup::flush()afterwards.
happenboard_event_submitted
Fires when a visitor’s event, sent from the Event Submission Form block, has been saved as a pending event with all its details. Use it to notify a channel, tag the submission or send it to an outside moderation queue.
Signature
do_action( 'happenboard_event_submitted', int $post_id, array<string,string> $data )
Parameters
-
$post_idint - The new event. Its status is always
pending. -
$dataarray<string,string> - Three keys:
submitter_name,submitter_email(trimmed and lowercased) andtitle.
Example
add_action(
'happenboard_event_submitted',
function ( $post_id, $data ) {
// Keep the submitter on file and tag the event for the review team.
update_post_meta( $post_id, '_my_submitter_ref', sanitize_email( $data['submitter_email'] ) );
wp_set_object_terms( $post_id, 'needs-review', 'happenboard_tag', true );
},
10,
2
);
Notes
- Two arguments: register with
10, 2. - The rest is already saved on the post: dates and time zone as
_happenboard_*meta, the submitter in_happenboard_submitter_nameand_happenboard_submitter_email, the category, if any, as ahappenboard_categoryterm. - It never fires for a refused submission (form turned off, login required, too many attempts, Turnstile failed) or one Akismet flags as spam. Events created in the admin, by import or through the REST API do not fire it.
- HappenBoard Pro emails the moderator from its own callback at priority 10. Yours runs alongside it.
happenboard_event_deleting
Fires when an event is deleted for good, before HappenBoard deletes its attendees one by one. Use it to delete what your plugin keeps per event.
Signature
do_action( 'happenboard_event_deleting', int $event_id, array<int,int> $attendee_ids )
Parameters
-
$event_idint - The event being deleted.
-
$attendee_idsarray<int,int> - IDs of its attendees, about to be deleted. On a translated event, only those who registered on this translation.
Example
add_action(
'happenboard_event_deleting',
function ( int $event_id, array $attendee_ids ): void {
global $wpdb;
// Remove what our plugin stores for this event.
$wpdb->delete( $wpdb->prefix . 'my_event_stats', array( 'event_id' => $event_id ), array( '%d' ) );
error_log( sprintf( 'Event %d deleted with %d attendee(s).', $event_id, count( $attendee_ids ) ) );
},
10,
2
);
Notes
- Two arguments: register with
10, 2to receive the attendee IDs. - Only a permanent deletion fires it: from the trash,
wp_delete_post( $id, true ), a forced REST or WP-CLI delete, thedelete-eventability. Moving an event to the trash deletes nothing. - The post, its meta and its attendee rows can still be read. Its date rows are already gone: read the dates from the event’s meta, not from the dates table.
- Each attendee is then deleted and fires
happenboard_attendee_deleted. HappenBoard Pro listens here first to delete the tickets, seat holds, sales and check-ins; when one translation is deleted alone, it moves the tickets to a translation that stays.
happenboard_event_description_allowed_html
Filters the HTML allowed in the Event Description block. The block runs the_content (or the_excerpt with its Excerpt only setting), then wp_kses() strips every tag and attribute missing from this list.
Signature
apply_filters( 'happenboard_event_description_allowed_html', array<string,array<string,mixed>> $allowed )
Parameters
-
$allowedarray<string,array<string,mixed>> - KSES list, tag name to allowed attributes:
wp_kses_allowed_html( 'post' )plusiframe,video,audio,source,trackand the form controls of HappenBoard’s form blocks.
Return value
array<string,array<string,mixed>>
The list passed to wp_kses(). It is cast with (array), so null or false strips every tag.
Example
add_filter(
'happenboard_event_description_allowed_html',
function ( $allowed ) {
// Allow collapsible sections in event descriptions.
$allowed['details'] = array( 'open' => true, 'class' => true );
$allowed['summary'] = array( 'class' => true );
return $allowed;
}
);
Notes
- One argument.
- Add to the list rather than removing what the block adds. Without those entries, oEmbed iframes, self-hosted media, or an RSVP Form or Ticket Picker placed in the description stop working.
- It runs after
the_content, so it cannot bring back markup another plugin removed. It does not run for a password-protected event (the block shows the password form) or an empty description.
happenboard_schema_event_type
Sets the @type of the Event JSON-LD. Return a schema.org Event subtype (MusicEvent, Festival, SportsEvent…) so search engines classify the event more precisely.
Signature
apply_filters( 'happenboard_schema_event_type', string $type, int $event_id, \WP_Post $post )
Parameters
-
$typestring - Default
'Event'. -
$event_idint - The event post ID.
-
$post\WP_Post - The event post.
Return value
string
The value written to @type, unchecked.
Example
add_filter(
'happenboard_schema_event_type',
function ( $type, $event_id, $post ) {
// Concerts are music events for search engines.
if ( has_term( 'concerts', 'happenboard_category', $post ) ) {
return 'MusicEvent';
}
return $type;
},
10,
3
);
Notes
- Three arguments: register with
10, 3. - It runs first in the schema chain, and
happenboard_schema_eventcan still overwrite@typelater. - Return an Event subtype. Another type keeps Event properties such as
startDate, and Google reports invalid structured data.
happenboard_schema_event
Filters the complete Event JSON-LD just before it is printed on the event page and returned by GET /happenboard/v1/events/{id}/schema. Use it to add properties or overwrite what HappenBoard built.
Signature
apply_filters( 'happenboard_schema_event', array<string,mixed> $schema, int $event_id, \WP_Post $post )
Parameters
-
$schemaarray<string,mixed> - The payload. Always has
@context,@type,name,description,startDate,endDate,eventStatus,inLanguage,url,eventAttendanceModeandlocation(aVirtualLocationwithout a venue).image,organizer,offers,isAccessibleForFree,maximumAttendeeCapacityandremainingAttendeeCapacityappear when they apply. -
$event_idint - The event post ID.
-
$post\WP_Post - The event post, already loaded.
Return value
array<string,mixed>|null
The array to encode. null removes the JSON-LD from the event page and makes the REST route answer 404. Any other type causes a fatal TypeError.
Example
add_filter(
'happenboard_schema_event',
function ( $schema, $event_id, $post ) {
// Add the age range and the event tags.
$schema['typicalAgeRange'] = (string) get_post_meta( $event_id, '_my_age_range', true );
$schema['keywords'] = wp_get_post_terms( $post->ID, 'happenboard_tag', array( 'fields' => 'names' ) );
return $schema;
},
10,
3
);
Notes
- Three arguments: register with
10, 3. startDateandendDateare those of the date the page describes: the one under way, else the next, else the last. An event with tickets but none on sale has nooffersand is not marked free.- On the event page it does not run when
happenboard_schema_disablereturns true or SEOPress holds a manual Event schema for the post. The REST route checks neither. - Drafts, previews and events without any date never reach it.
happenboard_event_rewrite_slug
Sets the event base, the first segment of every event address: events at /{base}/{event}/, the archive at /{base}/, the All dates page at /{base}/{event}/all-dates/. It overrides the Event base field under Settings → Permalinks.
Signature
apply_filters( 'happenboard_event_rewrite_slug', string $base )
Parameters
-
$basestring - The saved Event base, already cleaned, or
'events'when the field is empty.
Return value
string
The base. Each /-separated segment goes through sanitize_title(); an empty result falls back to events.
Example
add_filter(
'happenboard_event_rewrite_slug',
function ( $base ) {
// Events live at /agenda/{event}/.
return 'agenda';
}
);
Notes
- One argument. Add it from a plugin or
functions.php: the base is read when the post type is registered oninit. - Return the same value on every request. The rewrite rules are rebuilt on the next admin page load after the base changes, never on the front end: open any admin screen once after adding the filter.
- After a change, the old base is remembered (the last five) and its addresses redirect (301) to the new ones. The Event base field then says a filter sets the base.
- The All dates rule follows the base by itself: you do not need
happenboard_series_rewrite_prefixfor that.
happenboard_rewrite_rules_repaired
Fires after HappenBoard rebuilt the stored rewrite rules because they had lost its events’ and booking types’ rules, which makes those pages return 404. Another tool rebuilt the rules without HappenBoard loaded: use the action to alert someone, as the cause is likely to come back.
Signature
do_action( 'happenboard_rewrite_rules_repaired', array<int,string> $missing )
Parameters
-
$missingarray<int,string> - Post types that had no rule:
hboard_event,hboard_booking_typeor both.
Example
add_action(
'happenboard_rewrite_rules_repaired',
function ( array $missing ): void {
// Tell the site admin so they can look for the cause.
wp_mail(
get_option( 'admin_email' ),
'HappenBoard rebuilt the rewrite rules',
'Rules were missing for: ' . implode( ', ', $missing )
);
}
);
Notes
- One argument. The rules are already rebuilt and the repair is written to the PHP error log.
- The check runs on admin page loads only, at most once an hour, with pretty permalinks. Until someone opens the admin, the front end keeps answering 404.
- A change of event base also rebuilds the rules but does not fire this action.
happenboard_series_rewrite_prefix
Sets the first path segment of the rewrite rule behind the All dates page (/{base}/{event}/all-dates/). The default is the event base, so you only need it when event addresses were moved some other way.
Signature
apply_filters( 'happenboard_series_rewrite_prefix', string $prefix )
Parameters
-
$prefixstring - The event base (
eventsunless changed). It is used as a literal path.
Return value
string
The prefix. It only builds the default pattern handed to happenboard_series_rewrite_pattern.
Example
add_filter(
'happenboard_series_rewrite_prefix',
function ( $prefix ) {
// Our events were moved under /agenda/ by another plugin.
return 'agenda';
}
);
Notes
- One argument. It runs on
initat priority 25: register it earlier, then flush the rewrite rules (save Settings → Permalinks). HappenBoard flushes them by itself only when the event base changes. - HappenBoard links to the page with the event’s permalink followed by
all-dates/. The rule you build must still match those links.
happenboard_series_rewrite_pattern
Replaces the regex of the All dates rewrite rule. Use it when event addresses are not the event base followed by the event slug.
Signature
apply_filters( 'happenboard_series_rewrite_pattern', string $pattern )
Parameters
-
$patternstring - Default
'^' . preg_quote( $prefix, '/' ) . '/([^/]+)/all-dates/?$', with$prefixfromhappenboard_series_rewrite_prefix.
Return value
string
The regex passed to add_rewrite_rule(). Group 1 must capture the event slug: the target is index.php?post_type=hboard_event&name=$matches[1]&happenboard_view=series.
Example
add_filter(
'happenboard_series_rewrite_pattern',
function ( $pattern ) {
// Event addresses are /agenda/{category}/{slug}/.
return '^agenda/[^/]+/([^/]+)/all-dates/?$';
}
);
Notes
- One argument. Same timing as the prefix filter: register it before
initpriority 25, then flush the rewrite rules. - Keep the leading
^of the default. A capture group before the slug shifts it and the page answers 404. - Your pattern must match HappenBoard’s links to the page: the event’s permalink followed by
all-dates/.
happenboard_series_url
Returns the address of an event’s All dates page, for themes and plugins that link to it. HappenBoard answers it at priority 10 but does not use it for its own links.
Signature
apply_filters( 'happenboard_series_url', string $url, WP_Post|int $post )
Parameters
-
$urlstring - Value returned when
$postis not an event. Pass''. -
$postWP_Post|int - The event, as a post object or an ID.
Return value
string
The page address: the permalink followed by all-dates/, or the permalink with ?happenboard_view=series under plain permalinks. $url unchanged when the post is not an event.
Example
// In a theme template: link to every date of the current event.
$all_dates = apply_filters( 'happenboard_series_url', '', get_the_ID() );
if ( '' !== $all_dates ) {
printf(
'<a href="%s">%s</a>',
esc_url( $all_dates ),
esc_html__( 'All dates', 'my-theme' )
);
}
Notes
- Call it with
''then the post. Called with the post alone, a non-event post comes back unchanged as the value. - To change the address, hook it with
10, 2at a priority above 10, after HappenBoard. Its own See all dates link does not go through this filter.
happenboard_pro_gcal_event_payload
Filters the Google Calendar event HappenBoard Pro sends when it creates or updates an event’s copy. Add reminders, attendees or other Google fields, or change what was mapped.
Signature
apply_filters( 'happenboard_pro_gcal_event_payload', array<string,mixed> $payload, WP_Post $event )
Parameters
-
$payloadarray<string,mixed> - Google Event resource with
summary,description,location,status,extendedProperties,startandend, plusrecurrencefor a recurring event. -
$eventWP_Post - The event post. Read its meta with
get_post_meta().
Return value
array<string,mixed>
The resource sent to Google. It is cast with (array).
Example
add_filter(
'happenboard_pro_gcal_event_payload',
function ( $payload, $event ) {
// A pop-up reminder one hour before each event.
$payload['reminders'] = array(
'useDefault' => false,
'overrides' => array( array( 'method' => 'popup', 'minutes' => 60 ) ),
);
return $payload;
},
10,
2
);
Notes
- Two arguments: register with
10, 2. - Keep
extendedProperties.private.happenboard_origin. The calendar pull uses it to recognise HappenBoard’s own events; without it the next sync can import the event back as a duplicate. - The same payload creates and updates, and an update sends the whole resource: a key you remove is removed in Google too. An invalid field makes the push fail. Trashing or deleting an event sends separate requests that skip this filter.
- Adding
conferenceDatahas no effect: Pro does not send theconferenceDataVersionparameter Google needs to create a Meet link.
happenboard_pro_outlook_event_payload
Filters the Microsoft Graph event HappenBoard Pro sends to Outlook when it creates or updates an event’s copy. Same role as happenboard_pro_gcal_event_payload on the Google side.
Signature
apply_filters( 'happenboard_pro_outlook_event_payload', array<string,mixed> $payload, WP_Post $event )
Parameters
-
$payloadarray<string,mixed> - Graph Event resource with
subject,body,location,isAllDay,isCancelled,showAs,singleValueExtendedProperties,startandend, plusrecurrencewhen the repeat rule can be mapped. -
$eventWP_Post - The event post.
Return value
array<string,mixed>
The resource sent to Graph. It is cast with (array).
Example
add_filter(
'happenboard_pro_outlook_event_payload',
function ( $payload, $event ) {
$payload['categories'] = array( 'HappenBoard' );
// Put the title above the synced description.
$payload['body']['content'] = '<p>' . esc_html( get_the_title( $event ) ) . '</p>'
. $payload['body']['content'];
return $payload;
},
10,
2
);
Notes
- Two arguments: register with
10, 2. - Keep the
singleValueExtendedPropertiesentry whose ID ends inhappenboard_origin, for the same reason as on Google: without it, the event can be imported back. body.contentis HTML that ends with a “Synced from HappenBoard” footer, cut at 16,384 characters. Add your text before it, as in the example.- All-day events follow Graph’s rules (midnight to midnight of the next day, same time zone at both ends), so rewrite
startandendwith care. Trashing sends a separateisCancelledupdate and a permanent deletion deletes the copy; neither goes through this filter.
Next steps
- Attendees and sign-up hooks: react to registrations, cancellations and date changes.
- Block, schema and rendering hooks: the other Event schema filters and the block output.
- Calendar sync, webhook and check-in hooks: what happens after an event is pushed to Google or Outlook.