The ACF Flexible Content loop fails silently in five predictable ways. The function calls run, no PHP error is thrown, and the output is just empty. After fifteen years of building Flexible Content systems for agency clients (it is the foundation of every reusable component library I have shipped), I have seen each of these so many times that the diagnostic is now near-automatic. Here is the fix for each.
Jump to:
- Cause 1: Missing have_rows() entirely
- Cause 2: Wrong post context
- Cause 3: Missing the_row() inside the loop
- Cause 4: Using get_sub_field() outside the loop
- Cause 5: Nested loops without resetting
- The diagnostic order
Cause 1: Missing have_rows() entirely
The canonical Flexible Content loop uses have_rows() to drive iteration:
if ( have_rows( 'page_builder' ) ) {
while ( have_rows( 'page_builder' ) ) {
the_row();
$layout = get_row_layout();
get_template_part( 'template-parts/flexible-content/' . $layout );
}
}If you wrote it with get_field( 'page_builder' ) and tried to foreach over the result, you skipped the have_rows() mechanism entirely. The data is technically there, but get_sub_field() calls inside your loop will return nothing because there is no active row context.
Fix: rewrite the loop using have_rows() and the_row(). The full pattern is documented in the ACF have_rows() reference.
Cause 2: Wrong post context
have_rows() defaults to the current post in the loop. If you are calling it from a template that is not inside the_loop() (a footer template, a custom widget, a REST callback), the function does not know which post's data to read.
Symptoms: the loop returns false. var_dump( get_field( 'page_builder', get_the_ID() ) ) returns false too, even though the data exists in the database.
Fix: pass the post ID explicitly as the second argument:
if ( have_rows( 'page_builder', $post_id ) ) {
while ( have_rows( 'page_builder', $post_id ) ) {
the_row();
// ...
}
}For options pages, the second argument is 'option' rather than a post ID:
if ( have_rows( 'global_announcements', 'option' ) ) { /* ... */ }For users, taxonomies, and comments, the second argument follows the standard ACF post ID convention ('user_42', 'term_5', 'comment_99').
Cause 3: Missing the_row() inside the loop
This one is easy to miss and produces the most confusing symptom. The loop runs but every get_sub_field() returns nothing.
// Broken: missing the_row()
while ( have_rows( 'page_builder' ) ) {
$layout = get_row_layout(); // returns nothing
}the_row() is what advances the internal row pointer and makes the current row's sub-fields available. Without it, get_sub_field() and get_row_layout() have no row to read from.
Fix: add the_row(); as the first call inside every while ( have_rows() ) loop. This is per the ACF the_row() reference.
while ( have_rows( 'page_builder' ) ) {
the_row(); // <-- this
$layout = get_row_layout();
// ...
}Cause 4: Using get_sub_field() outside the loop
get_sub_field() only works inside a have_rows() loop with the_row() active. If you try to use it elsewhere, you get nothing back, and ACF does not throw an error.
// Broken: get_sub_field outside the loop returns nothing
$heading = get_sub_field( 'heading' ); // no active row contextFix: use get_field() if you are not inside a row, or wrap your code in the proper have_rows() / the_row() structure.
Cause 5: Nested loops without resetting
When you nest Flexible Content layouts (a "page builder" with a "stat row" layout that contains a repeater of stats), each level of nesting needs its own have_rows() / the_row() pair, and you must not break out of the outer loop before the inner one completes.
while ( have_rows( 'page_builder' ) ) {
the_row();
if ( get_row_layout() === 'stat_row' ) {
// Nested loop
if ( have_rows( 'stats' ) ) {
while ( have_rows( 'stats' ) ) {
the_row();
$label = get_sub_field( 'label' );
$value = get_sub_field( 'value' );
echo "<dt>$label</dt><dd>$value</dd>";
}
}
}
}Common bug: forgetting that the inner get_sub_field( 'label' ) reads from the inner row, not the outer. As long as you have called the_row() on the inner loop, this works correctly.
If you accidentally use the_field( 'page_builder' ) or another function that resets the outer pointer, the outer loop breaks mid-iteration. Stick to have_rows(), the_row(), and get_sub_field() exclusively inside the nested structure.
The diagnostic order
When a Flexible Content loop is silently empty, my order of checks:
- Confirm data exists.
var_dump( get_field( 'page_builder', get_the_ID() ) );should return an array. If false, the data is not there (wrong post ID, wrong field name, or the field group is not assigned to this post type). - Confirm have_rows() returns true. Add
var_dump( have_rows( 'page_builder' ) );directly before the loop. If false, the field name is wrong or you need to pass the post ID explicitly. - Confirm the_row() is called. Look at the first line inside
while ( have_rows() ). It must bethe_row();. - Confirm get_row_layout() returns the expected string. Add
var_dump( get_row_layout() );inside the loop. If it returns false, you are likely outside the row context. - Confirm sub-field names match the field group registration. Use
wp acf get(or check the field group export JSON) to see the actual sub-field names. Typos inget_sub_field( 'heading' )vsget_sub_field( 'heading_text' )are common.
The five causes above account for the overwhelming majority of "loop not working" tickets. If your debug session does not match any of them, the issue is usually in the field group registration itself (typo in the registered field name, wrong location rule) rather than in the template code. The full agency-scale pattern for Flexible Content is in Why Many Agencies Still Prefer ACF Over Gutenberg. For AI-assisted debugging of these patterns, see Using Claude CLI to Manage WordPress Sites.
Sources
Authoritative references this article was fact-checked against.
- Flexible Content field (Advanced Custom Fields docs)advancedcustomfields.com
- have_rows() function reference (ACF docs)advancedcustomfields.com
- the_row() function reference (ACF docs)advancedcustomfields.com





