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:
personandstory)
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
1076to your query loop’s actual ID - ACF Field Name: Change
person_story_relationshipto your field name - Post Types: Change
storyto 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_queryconflicts - 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
Dreamhost provides shell access for users. Sometimes it helps to view the logs for websites created in those users’ names.