Events, recurrence and series hooks

Available in:Free

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:

  1. happenboard_occurrences_syncing fires while the old rows are still there.
  2. The old rows are deleted, then the new ones are written. happenboard_occurrences_changed fires after each step.

When an event is deleted for good (moving it to the trash only removes its rows):

  1. Its rows are deleted and happenboard_occurrences_changed fires.
  2. happenboard_event_deleting fires. The post, its meta and its attendees can still be read.
  3. 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

Action Free src/Service/Events/OccurrenceSyncer.php

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_id int
The event whose dates are about to be rebuilt.
$announce bool
true when the rebuild comes from an edit (a save, a date skipped or moved). false when 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, 2 to 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

Action Free src/Service/Occurrences/Repository.php

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_id int
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_deleting fires.
  • 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

Filter Free src/Service/Occurrences/Repository.php

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

$clauses array{join:string,where:string}
SQL fragments, both empty by default. join is added after the join on the posts table (alias p; the dates table is o), where is ANDed with the rest. Both must be fully prepared.
$event_column string
Column holding the event ID. Always 'o.event_id' today.
$fallback string
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, 2 for the column, 10, 3 for 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 second prepare() 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

Filter Free src/Integration/Multilingual/TranslationGroup.php

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

$group array{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_id int
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

Action Pro src/REST/EventSubmissions.php

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_id int
The new event. Its status is always pending.
$data array<string,string>
Three keys: submitter_name, submitter_email (trimmed and lowercased) and title.

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_name and _happenboard_submitter_email, the category, if any, as a happenboard_category term.
  • 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

Action Free src/Service/Events/EventDeletion.php

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_id int
The event being deleted.
$attendee_ids array<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, 2 to 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, the delete-event ability. 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

Filter Free src/Block/EventDescription/Block.php

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

$allowed array<string,array<string,mixed>>
KSES list, tag name to allowed attributes: wp_kses_allowed_html( 'post' ) plus iframe, video, audio, source, track and 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

Filter Free src/Service/Schema/EventSchemaBuilder.php

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

$type string
Default 'Event'.
$event_id int
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_event can still overwrite @type later.
  • Return an Event subtype. Another type keeps Event properties such as startDate, and Google reports invalid structured data.

happenboard_schema_event

Filter Free src/Service/Schema/EventSchemaBuilder.php

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

$schema array<string,mixed>
The payload. Always has @context, @type, name, description, startDate, endDate, eventStatus, inLanguage, url, eventAttendanceMode and location (a VirtualLocation without a venue). image, organizer, offers, isAccessibleForFree, maximumAttendeeCapacity and remainingAttendeeCapacity appear when they apply.
$event_id int
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.
  • startDate and endDate are 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 no offers and is not marked free.
  • On the event page it does not run when happenboard_schema_disable returns 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

Filter Free src/Settings/Permalinks.php

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

$base string
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 on init.
  • 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_prefix for that.

happenboard_rewrite_rules_repaired

Action Free src/Settings/Permalinks.php

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

$missing array<int,string>
Post types that had no rule: hboard_event, hboard_booking_type or 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

Filter Free src/FSE/SeriesRewrite.php

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

$prefix string
The event base (events unless 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 init at 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

Filter Free src/FSE/SeriesRewrite.php

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

$pattern string
Default '^' . preg_quote( $prefix, '/' ) . '/([^/]+)/all-dates/?$', with $prefix from happenboard_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 init priority 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

Filter Free src/FSE/SeriesRewrite.php

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

$url string
Value returned when $post is not an event. Pass ''.
$post WP_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, 2 at a priority above 10, after HappenBoard. Its own See all dates link does not go through this filter.

happenboard_pro_gcal_event_payload

Filter Pro src/Service/Calendar/Push/EventMapper.php

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

$payload array<string,mixed>
Google Event resource with summary, description, location, status, extendedProperties, start and end, plus recurrence for a recurring event.
$event WP_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 conferenceData has no effect: Pro does not send the conferenceDataVersion parameter Google needs to create a Meet link.

happenboard_pro_outlook_event_payload

Filter Pro src/Service/Calendar/Push/OutlookEventMapper.php

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

$payload array<string,mixed>
Graph Event resource with subject, body, location, isAllDay, isCancelled, showAs, singleValueExtendedProperties, start and end, plus recurrence when the repeat rule can be mapped.
$event WP_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 singleValueExtendedProperties entry whose ID ends in happenboard_origin, for the same reason as on Google: without it, the event can be imported back.
  • body.content is 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 start and end with care. Trashing sends a separate isCancelled update and a permanent deletion deletes the copy; neither goes through this filter.

Next steps