Reverse Query an ACF Relationship Field

Quick Summary

  • Query the other post type with a meta_query that compares the relationship field using LIKE and the current post's ID wrapped in double quotes; it works with any ACF version.
  • Since ACF 6.2, the field's Bidirectional setting also writes the link to a Relationship field on the other post, but it only copies changes, so links saved earlier must be saved again.
  • For a relationship sub-field inside a repeater, match the row keys with compare_key REGEXP (WordPress 5.3+) or loop through the events in PHP.

An Advanced Custom Fields (ACF) Relationship field stores the link only on the post where you fill it in. If each event lists its sponsors, a sponsor page can still show its events with a reverse query: ask WordPress for the events whose relationship field contains the sponsor’s ID, using a meta_query that compares the field with LIKE and the ID wrapped in double quotes. Since ACF 6.2 there is a second way: turn on the field’s Bidirectional setting, and ACF also writes the link to a Relationship field on the sponsor, so the sponsor page only reads its own field. For the forward direction, see showing hand-picked related posts with an ACF Relationship field.

The scenario

There are two custom post types: Event and Sponsor. Every event has multiple sponsors, picked in a Relationship field named sponsors on the event.

Each sponsor also has a single page, and that page should show all the events the sponsor supported.

How to query the events that list the current sponsor

This code goes in the single sponsor template, for example single-sponsor.php, and works with any ACF version:

PHP
<?php
$sponsorID = get_the_ID();

$sponsored_events = new WP_Query( array(
    'post_type'      => 'event',
    'posts_per_page' => -1,
    'meta_query'     => array(
        array(
            'key'     => 'sponsors',
            'value'   => '"' . $sponsorID . '"',
            'compare' => 'LIKE',
        ),
    ),
) );

if ( $sponsored_events->have_posts() ) :
    while ( $sponsored_events->have_posts() ) : $sponsored_events->the_post();
        get_template_part( 'template-parts/event-card' );
    endwhile;
    wp_reset_postdata();
else : ?>
    <h3>No Sponsored Events Found</h3>
<?php endif; ?>
  • get_the_ID() is the sponsor whose page is being shown.
  • The meta_query looks at the sponsors field of every event and keeps the events whose value contains "12", if the sponsor’s ID is 12. WordPress adds the % wildcards around the value itself.
  • template-parts/event-card.php is the event card from the original project; replace it with your own markup.
  • wp_reset_postdata() restores the sponsor as the current post after the custom loop.

ACF uses the same meta_query in its guide to querying relationship fields.

Why is the ID wrapped in double quotes?

ACF saves the selected posts as a serialized array of IDs, and it stores each ID as a string. An event with sponsors 12 and 123 has this value in the database:

Code
a:2:{i:0;s:2:"12";i:1;s:3:"123";}

The quotes make the search match a whole ID. "12" matches this value, while "1" and "312" don’t. Without the quotes, a search for 12 would also return an event that only lists sponsor 123.

That only holds for values ACF saved. If other code wrote the field with update_post_meta() and integer IDs, the value looks like a:2:{i:0;i:12;i:1;i:123;} and the quoted search misses it. Save relationship values with ACF’s update_field(), which stores the IDs as strings.

ACF 6.2 added a Bidirectional setting to Relationship, Post Object, User and Taxonomy fields. Set it up once in the field groups:

  1. Add a Relationship field to the Sponsor post type, for example events, and limit it to the Event post type.
  2. Edit the event’s sponsors field and open its Advanced tab. Turn on Bidirectional.
  3. In Target Field, choose the sponsor’s events field and save the field group.

From then on, when you save an event, ACF adds the event’s ID to the events field of every sponsor you added to it, and removes it from every sponsor you removed. The sponsor template reads its own field, with the Return Format set to Post Object, in the same loop used to get post content from a Relationship field:

PHP
<?php
global $post;

$events = get_field( 'events' );

if ( $events ) :
    foreach ( $events as $post ) : setup_postdata( $post );
        get_template_part( 'template-parts/event-card' );
    endforeach;
    wp_reset_postdata();
endif;
?>

Things to know before you switch:

  • Only changes are copied. ACF compares the old and new selection when an event is saved. Links that existed before you turned the setting on aren’t copied, and saving an event again with the same sponsors doesn’t copy them either. Remove the sponsor, save, add it back and save again, or keep using the meta_query for older events.
  • The copy goes one way. Editing the events field on a sponsor doesn’t change the events. To make both sides editable, also turn on Bidirectional on the sponsor’s events field, with the event’s sponsors field as its target.
  • The target must be a top-level field. ACF’s bidirectional relationships documentation says target fields inside a Repeater or Group aren’t supported, and that the sync only runs where the field is shown on a post, user or taxonomy term, not on options pages or blocks.

Which approach should you use?

  • The meta_query needs no setup and no stored copies. It finds every event, including those saved long ago, works with any ACF version and also works when the field is inside a repeater (see below). The LIKE search runs over the meta_value column, which WordPress doesn’t index, so it gets slower as the number of events grows.
  • A bidirectional field suits new builds on ACF 6.2 or later. The sponsor page reads a field on its own post, and editors see each sponsor’s events on the sponsor’s edit screen. Links saved before you turned it on only show up after the sponsor is removed from the event and added again.

What if the relationship field is inside a repeater?

In the original version of this code, each event had a repeater called add_all_sponsors, and every row had a relationship sub-field called assign_sponsor. ACF saves each row under its own meta key: add_all_sponsors_0_assign_sponsor, add_all_sponsors_1_assign_sponsor and so on. Since WordPress 5.3, a meta_query can match those keys with a regular expression:

PHP
$sponsored_events = new WP_Query( array(
    'post_type'      => 'event',
    'posts_per_page' => -1,
    'meta_query'     => array(
        array(
            'key'         => '^add_all_sponsors_[0-9]+_assign_sponsor$',
            'compare_key' => 'REGEXP',
            'value'       => '"' . get_the_ID() . '"',
            'compare'     => 'LIKE',
        ),
    ),
) );

'compare_key' => 'LIKE' doesn’t work here, because WordPress escapes the % in the key, so it can’t act as a wildcard. The ^ and $ make the pattern match only the sub-field keys. Use the same loop as in the first example.

The original approach loops through every event and checks its sponsors in PHP. It is simple and fine for a modest number of events:

PHP
<?php
$sponsorID = get_the_ID();

$authblog_query = new WP_Query( array(
    'post_type'      => 'event',
    'posts_per_page' => -1,
) );

if ( $authblog_query->have_posts() ) :
    while ( $authblog_query->have_posts() ) : $authblog_query->the_post();

        $sponsorList = array();

        if ( have_rows( 'add_all_sponsors' ) ) :
            while ( have_rows( 'add_all_sponsors' ) ) : the_row();
                $sponsors = get_sub_field( 'assign_sponsor' );

                if ( $sponsors ) :
                    foreach ( $sponsors as $sponsor ) :
                        $sponsorList[] = $sponsor->ID;
                    endforeach;
                endif;
            endwhile;
        endif;

        if ( in_array( $sponsorID, $sponsorList ) ) :
            get_template_part( 'template-parts/event-card' );
        endif;

    endwhile;
    wp_reset_postdata();
endif;
?>
  1. Get the sponsor ID from the single sponsor page.
  2. Query all events.
  3. For each event, collect the sponsor IDs from every repeater row in $sponsorList.
  4. If the current sponsor’s ID is in that list, print the event card.

$sponsorList starts as an empty array for every event. Earlier versions of this code only unset the list after a match, so an event without sponsor rows reached in_array() with no list at all, which is a fatal TypeError on PHP 8. The sub-field’s Return Format must be Post Object, because the code reads $sponsor->ID.

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More