ACF Bidirectional Relationship + Kadence Query Loop Recipe
A step-by-step guide for creating custom queries that filter posts based on ACF relationship fields using Kadence Advanced Query Loop blocks.
Prerequisites
- Kadence Blocks Pro (for Advanced Query Loop)
- ACF (Advanced Custom Fields) with relationship field
- Code Snippets plugin (or access to functions.php)
- Two related post types (example:
person
andstory
)
Step 1: Set Up Your ACF Relationship Field
- Create a bidirectional relationship field in ACF
- Set it to connect your two post types (person ↔ story)
- Note the field name (example:
person_story_relationship
)
Step 2: Create Your Advanced Query Loop Block
- Add an Advanced Query Loop block to your template
- Design your query card layout (how each post should look)
- Find the block ID:
- Go to Kadence Blocks > All Queries
- Hover over your query’s edit link
- Note the ID from the browser status bar (example:
1076
)
Step 3: Create the Custom Query Function
Add this code via Code Snippets (or functions.php):
phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
// Replace 1076 with your actual block ID
if ( $ql_id == 1076 ) {
$person_id = get_queried_object_id();
$person_name = get_the_title($person_id);
$first_name = explode(' ', $person_name)[0];
// Replace 'person_story_relationship' with your ACF field name
$connected_stories = get_field('person_story_relationship', $person_id);
// If no connected posts, show a message
if (empty($connected_stories) || !is_array($connected_stories)) {
$no_results_post = new stdClass();
$no_results_post->ID = 999998;
$no_results_post->post_title = $first_name . " doesn't have any archived stories yet.";
$no_results_post->post_content = '';
$no_results_post->post_excerpt = '';
$no_results_post->post_status = 'publish';
$no_results_post->post_type = 'story'; // Replace with your target post type
$no_results_post->post_date = current_time('mysql');
$no_results_post->post_author = 0;
add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
return array( $no_results_post );
}, 10, 2 );
$query['post_type'] = 'story'; // Replace with your target post type
$query['posts_per_page'] = 1;
} else {
// Show connected posts
$query['post_type'] = 'story'; // Replace with your target post type
$query['post_status'] = 'publish';
$query['post__in'] = $connected_stories;
$query['posts_per_page'] = -1; // Show all connected posts
}
}
return $query;
}, 10, 3 );
Step 4: Customize for Your Use Case
Replace these values with your specifics:
- Block ID: Change
1076
to your query loop’s actual ID - ACF Field Name: Change
person_story_relationship
to your field name - Post Types: Change
story
to your target post type - No Results Message: Customize the message text as needed
Step 5: Test and Troubleshoot
Testing Steps
- Test on a page with connected posts – should show filtered results
- Test on a page with no connections – should show your custom message
- Clear all caches – very important for seeing changes
Common Issues
Filter not working at all:
- Check you’re using
kadence_blocks_pro_query_loop_query_vars
(note the_pro_
) - Verify the block ID is correct
- Ensure Code Snippets is activated
Wrong posts showing:
- Verify your ACF field name is correct
- Check that
get_queried_object_id()
returns the right page ID - Test what
get_field()
returns with debug code
Template not applying:
- Clear all caches (browser, plugin, server)
- Verify the query loop block design is saved properly
Debug Code Template
If something isn’t working, use this debug code to investigate:
phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
if ( $ql_id == YOUR_BLOCK_ID ) {
$page_id = get_queried_object_id();
$connected_posts = get_field('YOUR_FIELD_NAME', $page_id);
$debug_info = is_array($connected_posts) ? 'array_' . count($connected_posts) : 'not_array';
$test_post = new stdClass();
$test_post->ID = 999999;
$test_post->post_title = 'DEBUG: Page ' . $page_id . ' has ' . $debug_info;
$test_post->post_content = 'Connected IDs: ' . (is_array($connected_posts) ? implode(', ', $connected_posts) : 'none');
$test_post->post_excerpt = '';
$test_post->post_status = 'publish';
$test_post->post_type = 'YOUR_POST_TYPE';
$test_post->post_date = current_time('mysql');
$test_post->post_author = 0;
add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $test_post ) {
return array( $test_post );
}, 10, 2 );
$query['post_type'] = 'YOUR_POST_TYPE';
$query['posts_per_page'] = 1;
}
return $query;
}, 10, 3 );
Key Success Factors
- Correct Filter Name: Must be
kadence_blocks_pro_query_loop_query_vars
- Accurate Block ID: Get it from Kadence Blocks > All Queries
- Right ACF Field Name: Match exactly what you set in ACF
- Cache Clearing: Always clear caches when testing
- Context Awareness: Use
get_queried_object_id()
to get the current page ID
Multiple Post Types (Stories + Interviews)
If you need to query multiple related post types on the same page, you can handle them in one filter function:
Setup
- Create separate Advanced Query Loop blocks for each post type
- Note each block’s unique ID (example: stories = 1076, interviews = 1234)
- Create separate ACF relationship fields (or use the same field if they should share connections)
Combined Filter Code (Conflict-Free)
phpadd_filter( 'kadence_blocks_pro_query_loop_query_vars', function( $query, $ql_query_meta, $ql_id ) {
// Stories query (block ID 1076)
if ( $ql_id == 1076 ) {
$person_id = get_queried_object_id();
$person_name = get_the_title($person_id);
$first_name = explode(' ', $person_name)[0];
$connected_stories = get_field('person_story_relationship', $person_id);
if (empty($connected_stories) || !is_array($connected_stories)) {
$no_results_post = new stdClass();
$no_results_post->ID = 999998; // Unique ID for stories
$no_results_post->post_title = $first_name . " doesn't have any archived stories yet.";
$no_results_post->post_content = '';
$no_results_post->post_excerpt = '';
$no_results_post->post_status = 'publish';
$no_results_post->post_type = 'story';
$no_results_post->post_date = current_time('mysql');
$no_results_post->post_author = 0;
// IMPORTANT: Use specific markers to prevent conflicts
add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
if ( $wp_query->get( 'post_type' ) === 'story' && $wp_query->get( 'stories_empty_marker' ) ) {
return array( $no_results_post );
}
return $posts;
}, 10, 2 );
$query['post_type'] = 'story';
$query['posts_per_page'] = 1;
$query['stories_empty_marker'] = true; // Unique marker prevents conflicts
} else {
$query['post_type'] = 'story';
$query['post_status'] = 'publish';
$query['post__in'] = $connected_stories;
$query['posts_per_page'] = -1;
}
}
// Interviews query (block ID 1234)
if ( $ql_id == 1234 ) {
$person_id = get_queried_object_id();
$person_name = get_the_title($person_id);
$first_name = explode(' ', $person_name)[0];
$connected_interviews = get_field('person_interview_relationship', $person_id);
if (empty($connected_interviews) || !is_array($connected_interviews)) {
$no_results_post = new stdClass();
$no_results_post->ID = 999997; // Different ID for interviews
$no_results_post->post_title = $first_name . " doesn't have any archived interviews yet.";
$no_results_post->post_content = '';
$no_results_post->post_excerpt = '';
$no_results_post->post_status = 'publish';
$no_results_post->post_type = 'interview';
$no_results_post->post_date = current_time('mysql');
$no_results_post->post_author = 0;
// IMPORTANT: Use specific markers to prevent conflicts
add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
if ( $wp_query->get( 'post_type' ) === 'interview' && $wp_query->get( 'interviews_empty_marker' ) ) {
return array( $no_results_post );
}
return $posts;
}, 10, 2 );
$query['post_type'] = 'interview';
$query['posts_per_page'] = 1;
$query['interviews_empty_marker'] = true; // Unique marker prevents conflicts
} else {
$query['post_type'] = 'interview';
$query['post_status'] = 'publish';
$query['post__in'] = $connected_interviews;
$query['posts_per_page'] = -1;
}
}
return $query;
}, 10, 3 );
Key Points for Multiple Post Types
- One filter handles all blocks – cleaner than multiple separate filters
- Each block needs unique ID – target them individually with
if ( $ql_id == XXXX )
- Use different fake post IDs – prevents conflicts between no-results messages (999998, 999997, etc.)
- CRITICAL: Use unique markers – each post type needs its own marker (
stories_empty_marker
,interviews_empty_marker
) to preventposts_pre_query
conflicts - Separate ACF fields recommended – unless you want shared relationships
Adding More Post Types
To add additional post types (photos, documents, etc.), follow this pattern:
php// Photos query (block ID 1345)
if ( $ql_id == 1345 ) {
$person_id = get_queried_object_id();
$person_name = get_the_title($person_id);
$first_name = explode(' ', $person_name)[0];
$connected_photos = get_field('person_photo_relationship', $person_id);
if (empty($connected_photos) || !is_array($connected_photos)) {
$no_results_post = new stdClass();
$no_results_post->ID = 999996; // Unique ID
$no_results_post->post_title = $first_name . " doesn't have any archived photos yet.";
$no_results_post->post_content = '';
$no_results_post->post_excerpt = '';
$no_results_post->post_status = 'publish';
$no_results_post->post_type = 'photo';
$no_results_post->post_date = current_time('mysql');
$no_results_post->post_author = 0;
add_filter( 'posts_pre_query', function( $posts, $wp_query ) use ( $no_results_post ) {
if ( $wp_query->get( 'post_type' ) === 'photo' && $wp_query->get( 'photos_empty_marker' ) ) {
return array( $no_results_post );
}
return $posts;
}, 10, 2 );
$query['post_type'] = 'photo';
$query['posts_per_page'] = 1;
$query['photos_empty_marker'] = true; // Unique marker
} else {
$query['post_type'] = 'photo';
$query['post_status'] = 'publish';
$query['post__in'] = $connected_photos;
$query['posts_per_page'] = -1;
}
}
Remember: Each new post type needs:
- Unique block ID
- Unique fake post ID (999996, 999995, etc.)
- Unique marker name (
photos_empty_marker
,documents_empty_marker
, etc.) - Corresponding ACF relationship field
Final Notes
- This method works for any ACF relationship field setup
- The query preserves your designed template layout
- Custom “no results” messages provide better UX than empty sections
- Always test on both connected and unconnected posts
- You can add as many post types as needed by following the same pattern