Filter events with FacetWP

The Events Calendar logoYou can use FacetWP without issues with the free version of The Events Calendar. Just retrieve the tribe_events post type with any supported listing template, and filter them with facets. If you are using the Pro version, read on about its issues and fixes.

Note that FacetWP does not work with the default calendar view template that The Events Calendar uses on the events post type archive. If you want to filter events, you’ll have to create and use your own listing template.

Fix issues with The Events Calendar Pro

If you have the The Events Calendar Pro installed (together with the free The Events Calendar plugin), you will run into empty, non-functional facets.

With the Pro version of TEC, you have access to recurring events, which may be the main reason to buy and install it. The cause of the issue with empty facets is how the Pro version handles these recurring events. This issue happens whether you are actually using recurring events or not.

Below we will describe the deeper cause of the issue, and two ways to fix this. Both ways will work to solve the empty facets issue, whether you are using recurring events or not.

Recurring events in The Events Calendar Pro

TEC Pro uses a custom database table to store recurring events. Because posts in custom tables are not indexed by FacetWP, TEC Pro is basically incompatible with FacetWP, leading to empty facets on pages that retrieve (only) tribe_events posts.

Before TEC Pro version 6.0.0, the post_parent field was used to relate a recurring event with its child “occurrences”. A recurring event was a post representing the event’s first occurrence, and the subsequent occurrences were posts related to the first by their post_parent field.

In TEC Pro version 6.0.0, TEC introduced a new implementation called “CT1”, which stands for Custom Tables v1. In this new structure, events and their child occurrences are stored in a custom tec_occurrences database table. In this table, the main events and all child occurrences each have an individual occurrence_id, also called a “provisional post ID”. This way, they can be treated as “normal” posts by TEC functions. Note that the main event is also still stored in the wp_posts table.

For a full explanation and all terminology in CT1, see this TEC documentation page.

Recurring events and FacetWP

FacetWP in general is incompatible with posts in custom tables: it only indexes posts in WP’s wp_posts table, so event occurrences are not indexed. Which leaves only the main/parent events in wp_posts to be indexed.

You’d expect this to lead to only parent events being indexed by FacetWP, and occurrences not. But this is not the case, as TEC Pro also treats parent events as occurrences. It then hijacks every query that has its post_type argument set to (only) tribe_events, to use its custom tec_occurrences table, and inserts occurrence IDs into it. It also adds 10000000 to the post ID count, presumably to prevent collisions with normal post IDs. You can see this happening if you type FWP.settings.debug.sql in your browser Console, on a page with a tribe_events query and facets:

The Events Calendar Pro hijacking the query, as seen in Debug Mode with FWP.settings.debug.sql.
The Events Calendar Pro hijacking the query, as seen in Debug Mode with FWP.settings.debug.sql.

Because occurrences in the custom tec_occurrences database table are not indexed, this query injection will result in empty facets, without any options, and non-functional filtering.

There are two ways to deal with this:

Two ways to let FacetWP work with TEC Pro

There are two approaches to fix the issue with empty facets when using TEC Pro. Both approaches will fix the empty facets issue, whether you are actually using recurring events or not. Which approach to choose depends on what you want to do with the recurring events in the query (if you are using them).

Option 1 will make FacetWP index and filter (parent) events and any recurring events that are in the query.

Option 2 has two parts: the first part will fix the empty facet issue, but will remove recurring events from the query. With the second part you can then optionally add them back in, under each parent event in the loop (with a few caveats).

Option 1: Force FacetWP to index recurring events

The first way to make FacetWP filter recurring events is to use the facetwp_indexer_row_data hook to look them up for each parent event while indexing, and add new rows to the index table for each recurring event found.

To accomplish this, add the following snippet to your (child) theme’s functions.php. In line 10, add the names of the facets you are using on the page where you have your events query/listing. Don’t include any names of Pager, Sort, Search, or Reset facets (these facet types are not indexed).

After adding (or editing) the below code, make sure to do a full re-index. Now your facets’ choices should reflect the parent and recurring events in your listing.

How to use custom PHP code?

PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info

// Make FacetWP index recurring events add_filter( 'facetwp_indexer_row_data', function( $rows, $params ) { if ( ! function_exists( 'tribe_events' ) ) { return $rows; } // Add the facet(s) on your events page to the array. // Don't include Pager, Sort, Search or Reset facets. $events_facets = [ 'my_event_facet' , 'my_other_event_facet' ]; if ( in_array( $params['facet']['name'], $events_facets ) ) { foreach ( $rows as $row ) { $post_id = $row['post_id']; // Get all occurrence IDs for a "parent" event. // This includes the parent event itself, which is also an occurrence. $event_ids = tribe_events()->where( 'ID', $post_id )->pluck( 'ID' ); if ( ! empty( $event_ids ) ) { foreach ( $event_ids as $event_id ) { $new_row = $row; $new_row['post_id'] = $event_id; $rows[] = $new_row; } } } } return $rows; }, 10, 2 );

Option 2: Remove recurring events from the main query and (optionally) re-add them later

The above-mentioned query hijack (that adds the recurring child events) only happens if the post_type argument of the query is set to only tribe_events. This presents an opportunity for a workaround to get TEC Pro working with FacetWP.

If you add a second post type, the query hijack will not happen, and the main parent events will be retrieved normally from the wp_posts table. This second post type could be anything, and it does not even have to be an existing post type. In a custom WP_Query, the following will work. You can apply the same query arguments in a Listing Builder listing in Dev mode, or with a query argument hook (e.g. pre_get_posts or a dedicated hook) in any other listing template type.

How to use custom PHP code?

PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info

$args = [ 'post_type' => [ 'tribe_events', 'x' // The fix: add a non-existing post_type ], 'post_status' => [ 'publish' ], 'posts_per_page' => 10, 'orderby' => 'meta_value', // order by event start date 'order' => 'ASC', 'meta_key' => '_EventStartDate', 'meta_value' => date( 'Y-m-d H:i:s' ), // now 'meta_compare' => '>', // show only future events (later than 'now') 'facetwp' => true // needed in a custom WP_Query, not in Listing Builder listings (in Dev mode) ]; $events_query = new WP_Query( $args );

With the above query arguments in place, only main (parent) events are retrieved. There will be no recurring events (“occurrences”) in your listing anymore.

So if you don’t use recurring events, adding a second (fake) post type is enough to get facets working again with TEC Pro installed. You can adjust all other query arguments to what you need.

In the above query example, we filter for events with a start date in the future. However, if you do want to add in recurring events (with the workaround described below), don’t do this. In this case, all events need to be retrieved, even if they are in the past, because there could be child occurrences with a start date still in the future:

How to use custom PHP code?

PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info

$args = [ 'post_type' => [ 'tribe_events', 'x' // The fix: add a non-existing post_type ], 'post_status' => [ 'publish' ], 'posts_per_page' => 10, 'orderby' => 'meta_value', // order by event start date 'order' => 'ASC', 'meta_key' => '_EventStartDate', 'facetwp' => true // needed in a custom WP_Query, not in Listing Builder listings (in Dev mode) ]; $events_query = new WP_Query( $args );
Add occurrences back into the listing (optional)

If you want to add occurrences back into the loop after removing them by changing the post_type argument, we can now get all child occurrences for each parent event in the loop:

How to use custom PHP code?

PHP code can be added to your (child) theme's functions.php file. Alternatively, you can use the Custom Hooks add-on, or a code snippets plugin. More info

if ( $events_query->have_posts() ) : while ( $events_query->have_posts() ) : $events_query->the_post(); // Get all occurrence IDs for a "parent" event. // This includes the parent event itself, which is also an occurrence. $occurrence_ids = tribe_events()->where( 'ID', get_the_ID() )->pluck( 'ID' ); foreach ( $occurrence_ids as $occurrence_id ) { $start_date = get_post_meta( $occurrence_id, '_EventStartDate', true ); $start_date_formatted = tribe_get_start_date( $occurrence_id, false, 'F j, Y - H:i' ); $end_date_formatted = tribe_get_end_date( $occurrence_id, false,'F j, Y - H:i' ); $title = get_post( $occurrence_id )->post_title; $permalink = get_the_permalink( $occurrence_id ); // Only show occurrence if its start date is after "now": if ( $start_date > date( 'Y-m-d H:i:s' ) ) { echo '<h2><a href="' . $permalink . '">' . $title . '</a></h2>'; echo '<p>From: ' . $start_date_formatted . ' to '. $end_date_formatted . '</p>'; } } endwhile; else : _e( 'Sorry, no posts matched your criteria.' ); endif; wp_reset_postdata();

In this loop, parent events and all their child occurrences are retrieved, as long as their start dates are in the future (after now). The parent events themselves will also be retrieved because TEC Pro treats them as an individual occurrence.

Notice that for the retrieval of the start- or end dates of the occurrences, you need to pass the occurrence_id, not the post_id. For the permalink, post title, thumbnail, or other features of the main event, you can use either the occurrence_id or the main event’s post ID (retrieved with get_the_ID()).

You could use the above loop template as a starting point to build several different layouts. Implemented as above, it displays all occurrences as individual events, each with its own heading, mimicking how TEC Pro listings would work without FacetWP. Another idea could be to show a list of occurrences (as items, dates, or links) under their parent event’s heading/description.

See The Events Calendar’s developer documentation for more functions to use with (recurring) events.

Limitations

A few limitations and things to keep in mind if you add recurrences back into the loop:

  • Facets, including Sort facets, will filter (or order) the parent events, not the individual child occurrences.
  • If the query is ordering by _EventStartDate, be aware that this is the parent event’s start date, even if that parent event/occurence is not showing anymore because its start date is now in the past.
  • Occurrences will show up grouped by parent event.

See also

Last updated: July 16, 2025