Skip to main content

Repeater, Group, Clone, and Flexible Content Patterns

ACF Pro structural fields become an advantage only when you model clear contracts and enforce safe rendering behavior.

Learning Focus

You will learn how to choose between Group, Repeater, Clone, and Flexible Content, implement production-safe render loops, and validate row/layout integrity with WP-CLI. This matters because structural fields are where ACF projects either become scalable or become impossible to maintain.

Concept Overview

Group, Repeater, Clone, and Flexible Content solve different structural problems. Group models one fixed object. Repeater models a list of similar objects. Clone lets you reuse schema fragments. Flexible Content models variable layout composition where row types can differ.

The biggest mistake is to treat these as interchangeable. They are not. Repeater adds loop complexity. Flexible Content adds branching complexity. Clone adds dependency complexity. The right choice depends on whether your data shape is fixed, repeated, or polymorphic.

In mature ACF Pro projects, these field types often appear together: a Flexible Content layout may include a cloned CTA object and one nested repeater. That can be powerful, but it requires strict rendering guards, clear layout naming, and CLI audits to avoid silent breakage.

Core Idea

Pick the least complex structural field that matches the data shape, then enforce robust render guards for empty, unknown, or malformed rows.

Why It Matters

ApproachWhat HappensImpact in Production
Group for fixed blocks, Repeater for repeated itemsRendering logic stays predictableLower defect rate and easier onboarding
Flexible Content used only where layouts truly varyLayout logic remains intentionalBetter maintainability and faster QA cycles
Clone shared CTA/metadata fragmentsSchema reuse without copy-paste driftConsistent behavior across templates
Guard unknown layouts and empty rows explicitlyErrors are visible instead of silentFaster incident triage and safer releases
Deep uncontrolled nesting (wrong pattern)Query and render complexity explodesSlow pages, fragile templates, expensive refactors

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
Group field'type' => 'group', 'sub_fields' => [...]Model one fixed object with named propertiesACF Pro Required
Repeater field'type' => 'repeater', 'sub_fields' => [...]Model ordered rows of same shapeACF Pro Required
Clone field'type' => 'clone', 'clone' => ['field_xxx']Reuse existing field definitionsACF Pro Required
Flexible Content'type' => 'flexible_content', 'layouts' => [...]Compose pages from variable row layoutsACF Pro Required
have_rows()`have_rows(string $selector, intstring $post_id = false): bool`Iterate rows in Repeater/Flexible
the_row()the_row(): voidMove loop pointer to current rowMust be called inside while (have_rows(...))
get_sub_field()get_sub_field(string $selector, bool $format_value = true): mixedRead row sub-fieldPrefer null/empty guards in templates
get_row_layout()`get_row_layout(): stringfalse`Resolve active Flexible layout slug
acf_get_field_groups()acf_get_field_groups(array $filter = []): arrayRuntime model auditPair with acf_get_fields() in CLI checks
ACF Pro Required

All four structural field types covered in this lesson (Group, Repeater, Clone, Flexible Content) require ACF Pro.

Practical Use Cases

Use Case 1 — Build a service FAQ model with Group + Repeater and strict output guards

A clinic service page uses one hero object and a list of FAQs. You need fixed hero structure plus repeated Q/A rows, with safe rendering when some rows are incomplete.

  1. Register Group field for service hero object.
  2. Register Repeater for FAQ rows.
  3. Render Group values only when required keys exist.
  4. Loop FAQ rows with have_rows() and skip incomplete entries.
  5. Verify model shape and row payload via CLI.
wp-content/themes/clinic-pro/inc/acf/service-structural-model.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_service_structural_model',
'title' => 'Service Structural Model',
'fields' => [
[
'key' => 'field_service_hero_group',
'label' => 'Service Hero Group',
'name' => 'service_hero_group',
'type' => 'group',
'layout' => 'block',
'sub_fields' => [
[
'key' => 'field_service_hero_headline',
'label' => 'Hero Headline',
'name' => 'service_hero_headline',
'type' => 'text',
'required' => 1,
],
[
'key' => 'field_service_hero_summary',
'label' => 'Hero Summary',
'name' => 'service_hero_summary',
'type' => 'textarea',
],
],
],
[
'key' => 'field_service_faq_items',
'label' => 'Service FAQ Items',
'name' => 'service_faq_items',
'type' => 'repeater',
'layout' => 'row',
'button_label' => 'Add FAQ',
'sub_fields' => [
[
'key' => 'field_service_faq_question',
'label' => 'Question',
'name' => 'service_faq_question',
'type' => 'text',
'required' => 1,
],
[
'key' => 'field_service_faq_answer',
'label' => 'Answer',
'name' => 'service_faq_answer',
'type' => 'textarea',
'required' => 1,
],
],
],
],
'location' => [[[
'param' => 'page_template',
'operator' => '==',
'value' => 'template-service.php',
]]],
]);
});

add_action('template_redirect', function (): void {
if (!is_page_template('template-service.php')) {
return;
}

$hero = get_field('service_hero_group');
if (is_array($hero) && !empty($hero['service_hero_headline'])) {
echo '<h1>' . esc_html((string) $hero['service_hero_headline']) . '</h1>';
echo '<p>' . esc_html((string) ($hero['service_hero_summary'] ?? '')) . '</p>';
}

if (have_rows('service_faq_items')) {
echo '<section class="service-faq">';

while (have_rows('service_faq_items')) {
the_row();
$question = (string) get_sub_field('service_faq_question');
$answer = (string) get_sub_field('service_faq_answer');

if ($question === '' || $answer === '') {
continue;
}

echo '<details><summary>' . esc_html($question) . '</summary><div>' . wp_kses_post($answer) . '</div></details>';
}

echo '</section>';
}
});
terminal: command
wp post create --post_title="Structural FAQ Drill" --post_status=publish --post_type=page
wp eval '$id=(int)get_page_by_title("Structural FAQ Drill", OBJECT, "page")->ID; update_field("service_hero_group", ["service_hero_headline"=>"Laser Treatment Plans","service_hero_summary"=>"Evidence-based protocols"], $id); update_field("service_faq_items", [["service_faq_question"=>"Is it painful?","service_faq_answer"=>"Usually mild discomfort."],["service_faq_question"=>"Downtime?","service_faq_answer"=>"Minimal."]], $id); echo get_field("service_hero_group",$id)["service_hero_headline"] . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("Structural FAQ Drill", OBJECT, "page")->ID; $rows=get_field("service_faq_items",$id); echo count($rows) . PHP_EOL; echo $rows[0]["service_faq_question"] . PHP_EOL;'
terminal: output
Success: Created post 1345.
Laser Treatment Plans
2
Is it painful?
note

Group + Repeater is ideal when one section has fixed metadata and one section has repeated rows.

Use Case 2 — Compose landing pages with Flexible Content + Clone CTA fragment

A growth team needs reusable page layouts where each landing page can combine Hero, Logos, and Testimonial sections in different orders. CTA fields must stay consistent across layouts.

  1. Register a shared CTA fragment field group.
  2. Register Flexible Content layouts for hero_section, logos_section, and testimonial_section.
  3. Clone CTA fragment into relevant layouts.
  4. Render layouts via switch (get_row_layout()) with strict guards.
  5. Verify layout map and cloned payload through CLI.
wp-content/themes/clinic-pro/inc/acf/landing-flex-model.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_shared_cta_fragment',
'title' => 'Shared CTA Fragment',
'fields' => [
[
'key' => 'field_shared_cta_label',
'label' => 'CTA Label',
'name' => 'shared_cta_label',
'type' => 'text',
],
[
'key' => 'field_shared_cta_url',
'label' => 'CTA URL',
'name' => 'shared_cta_url',
'type' => 'url',
],
],
'location' => [[[
'param' => 'options_page',
'operator' => '==',
'value' => 'acf-options-global-cta',
]]],
]);

acf_add_local_field_group([
'key' => 'group_landing_sections',
'title' => 'Landing Sections',
'fields' => [
[
'key' => 'field_landing_sections',
'label' => 'Landing Sections',
'name' => 'landing_sections',
'type' => 'flexible_content',
'button_label' => 'Add Section',
'layouts' => [
[
'key' => 'layout_hero_section',
'name' => 'hero_section',
'label' => 'Hero Section',
'display' => 'block',
'sub_fields' => [
[
'key' => 'field_hero_title',
'label' => 'Hero Title',
'name' => 'hero_title',
'type' => 'text',
],
[
'key' => 'field_hero_cta_clone',
'label' => 'Hero CTA',
'name' => 'hero_cta_clone',
'type' => 'clone',
'clone' => ['field_shared_cta_label', 'field_shared_cta_url'],
'display' => 'seamless',
],
],
],
[
'key' => 'layout_logos_section',
'name' => 'logos_section',
'label' => 'Logos Section',
'display' => 'block',
'sub_fields' => [
[
'key' => 'field_logos_repeater',
'label' => 'Logos',
'name' => 'logos_repeater',
'type' => 'repeater',
'sub_fields' => [
[
'key' => 'field_logo_image_id',
'label' => 'Image ID',
'name' => 'logo_image_id',
'type' => 'number',
],
],
],
],
],
[
'key' => 'layout_testimonial_section',
'name' => 'testimonial_section',
'label' => 'Testimonial Section',
'display' => 'block',
'sub_fields' => [
[
'key' => 'field_testimonial_quote',
'label' => 'Quote',
'name' => 'testimonial_quote',
'type' => 'textarea',
],
],
],
],
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);
});

add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}

if (!have_rows('landing_sections')) {
return;
}

while (have_rows('landing_sections')) {
the_row();
$layout = get_row_layout();

switch ($layout) {
case 'hero_section':
$title = (string) get_sub_field('hero_title');
$ctaLabel = (string) get_sub_field('shared_cta_label');
$ctaUrl = (string) get_sub_field('shared_cta_url');
if ($title !== '') {
echo '<section class="hero"><h2>' . esc_html($title) . '</h2>';
if ($ctaLabel !== '' && $ctaUrl !== '') {
echo '<a href="' . esc_url($ctaUrl) . '">' . esc_html($ctaLabel) . '</a>';
}
echo '</section>';
}
break;

case 'logos_section':
echo '<section class="logos"></section>';
break;

case 'testimonial_section':
$quote = (string) get_sub_field('testimonial_quote');
if ($quote !== '') {
echo '<blockquote>' . esc_html($quote) . '</blockquote>';
}
break;

default:
error_log('[landing-flex] unknown layout=' . (string) $layout);
}
}
});
terminal: command
wp eval '$rows=[["acf_fc_layout"=>"hero_section","hero_title"=>"Clinic Hero","shared_cta_label"=>"Book Now","shared_cta_url"=>"/book"],["acf_fc_layout"=>"testimonial_section","testimonial_quote"=>"Great outcomes."]]; update_field("landing_sections",$rows,1345); $saved=get_field("landing_sections",1345); echo count((array)$saved) . PHP_EOL; echo $saved[0]["acf_fc_layout"] . PHP_EOL;'
wp eval '$rows=get_field("landing_sections",1345); foreach((array)$rows as $row){echo $row["acf_fc_layout"] . PHP_EOL;}'
terminal: output
2
hero_section
hero_section
testimonial_section
ACF Pro Required

Flexible Content and Clone patterns shown here are ACF Pro features.

Use Case 3 — Edge case: unguarded nested structures crash or degrade rendering

A team introduces nested repeaters and new layouts without updating template guards. Unknown layout slugs and empty rows begin hitting production logs, and pages intermittently break.

❌ Fragile Pattern

wp-content/themes/clinic-pro/template-parts/flex-fragile.php
<?php

declare(strict_types=1);

add_action('template_redirect', function (): void {
if (!have_rows('landing_sections')) {
return;
}

while (have_rows('landing_sections')) {
the_row();
$layout = get_row_layout();

if ($layout === 'hero_section') {
echo '<h2>' . get_sub_field('hero_title') . '</h2>';
}

if ($layout === 'logos_section') {
$logos = get_sub_field('logos_repeater');
foreach ($logos as $logo) {
echo wp_get_attachment_image((int) $logo['logo_image_id'], 'medium');
}
}
}
});

✅ Robust Pattern

wp-content/themes/clinic-pro/template-parts/flex-robust.php
<?php

declare(strict_types=1);

add_action('template_redirect', function (): void {
if (!is_page() || !have_rows('landing_sections')) {
return;
}

$maxRows = 20;
$processed = 0;

while (have_rows('landing_sections')) {
the_row();
$processed++;

if ($processed > $maxRows) {
error_log('[flex-robust] row limit exceeded on page=' . get_the_ID());
break;
}

$layout = (string) get_row_layout();

switch ($layout) {
case 'hero_section':
$title = (string) get_sub_field('hero_title');
if ($title !== '') {
echo '<section class="hero"><h2>' . esc_html($title) . '</h2></section>';
}
break;

case 'logos_section':
$logos = get_sub_field('logos_repeater');
if (!is_array($logos) || count($logos) === 0) {
continue 2;
}

echo '<section class="logos">';
foreach ($logos as $logo) {
$id = (int) ($logo['logo_image_id'] ?? 0);
if ($id > 0) {
echo wp_get_attachment_image($id, 'medium');
}
}
echo '</section>';
break;

case 'testimonial_section':
$quote = (string) get_sub_field('testimonial_quote');
if ($quote !== '') {
echo '<blockquote>' . esc_html($quote) . '</blockquote>';
}
break;

default:
error_log('[flex-robust] unknown layout=' . $layout . ' page=' . get_the_ID());
}
}
});
terminal: command
wp eval '$rows=get_field("landing_sections",1345); echo "rows=" . count((array)$rows) . PHP_EOL; foreach((array)$rows as $i=>$row){echo "#".($i+1).":" . ($row["acf_fc_layout"] ?? "missing") . PHP_EOL;}'
wp eval '$rows=get_field("landing_sections",1345); $known=["hero_section","logos_section","testimonial_section"]; $unknown=[]; foreach((array)$rows as $row){$layout=$row["acf_fc_layout"] ?? ""; if($layout!=="" && !in_array($layout,$known,true)){$unknown[]=$layout;}} print_r($unknown);'
terminal: output
rows=2
#1:hero_section
#2:testimonial_section
Array
(
)
warning

Flexible Content requires explicit unknown-layout handling. Silent failure here can hide schema drift for weeks.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Using Flexible Content where Repeater is enoughOverestimating layout variabilityUnnecessary branching and maintenance costUse Repeater when row shape is fixed
Missing default branch in layout switchAssumes layout list never changesNew layout silently not renderedAdd default with logging and CLI unknown-layout audit
Deep nested repeaters without limitsNo complexity budget for content modelSlow rendering and high memory usageEnforce row caps and split sections
Cloning shared fragments without naming clarityClone source ambiguous to developersMisconfigured fields and broken retrievalPrefix clone-related names and document source keys
Rendering subfields without empty checksAssumes all rows are completeBroken markup and blank wrappersGuard each critical subfield before output
No CLI payload verification before releaseReliance on editor spot checksRuntime data shape mismatches go unnoticedAdd wp eval row/layout audits in release checklist
Deep Dive: Why Flexible Content Failures Often Look Random

Flexible Content failures usually surface as intermittent rendering bugs because only specific pages contain problematic layout combinations. One page may work perfectly while another breaks due to an unknown layout slug or incomplete row payload. These defects evade simple smoke tests that check only one sample page. The solution is to audit saved layout arrays directly and compare them to your template switch coverage. Treat layout slugs as contracts and validate them through CLI before deploy.

wp eval '$rows=get_field("landing_sections",1345); $known=["hero_section","logos_section","testimonial_section"]; foreach((array)$rows as $row){$layout=$row["acf_fc_layout"] ?? "missing"; echo $layout . (in_array($layout,$known,true) ? " [ok]" : " [unknown]") . PHP_EOL;}'

Best Practices

  1. Model with the simplest structural type first: Group/Repeater before Flexible when possible.
  2. Keep layout slugs stable and semantic: hero_section, logos_section, testimonial_section.
  3. Always guard empty rows and required subfields: avoid emitting empty wrappers.
  4. Include default branch logging in every Flexible switch: unknown layouts should be visible.
  5. Use Clone for repeated schema fragments, not copy-paste duplication.
  6. Set pragmatic row limits for heavy sections and enforce them in render code.
  7. Run CLI audits for row counts and layout maps before each release.

Hands-On Practice

Exercise 1: Register Group + Repeater service model

Create wp-content/themes/clinic-pro/inc/acf/service-structural-model.php and run:

wp eval '$g=acf_get_field_groups(["key"=>"group_service_structural_model"]); echo count($g) . PHP_EOL;'

After completing this exercise, output should be:

1

Exercise 2: Seed FAQ rows and verify row count

Run:

wp eval '$id=1345; update_field("service_faq_items", [["service_faq_question"=>"Q1","service_faq_answer"=>"A1"],["service_faq_question"=>"Q2","service_faq_answer"=>"A2"]], $id); $rows=get_field("service_faq_items",$id); echo count($rows) . PHP_EOL;'

After completing this exercise, output should be:

2

Exercise 3: Register Flexible model and store two layouts

Run:

wp eval '$id=1345; $rows=[["acf_fc_layout"=>"hero_section","hero_title"=>"Demo Hero","shared_cta_label"=>"Book","shared_cta_url"=>"/book"],["acf_fc_layout"=>"testimonial_section","testimonial_quote"=>"Excellent service."]]; update_field("landing_sections",$rows,$id); echo count((array)get_field("landing_sections",$id)) . PHP_EOL;'

After completing this exercise, output should be:

2

Exercise 4: Audit unknown layouts

Run:

wp eval '$rows=get_field("landing_sections",1345); $known=["hero_section","logos_section","testimonial_section"]; $unknown=[]; foreach((array)$rows as $row){$layout=$row["acf_fc_layout"] ?? ""; if($layout!=="" && !in_array($layout,$known,true)){$unknown[]=$layout;}} print_r($unknown);'

After completing this exercise, expected output:

Array
(
)

Exercise 5: Validate cloned CTA payload presence

Run:

wp eval '$rows=get_field("landing_sections",1345); foreach((array)$rows as $row){if(($row["acf_fc_layout"] ?? "")==="hero_section"){echo ($row["shared_cta_label"] ?? "") . " | " . ($row["shared_cta_url"] ?? "") . PHP_EOL;}}'

After completing this exercise, output should be:

Book | /book

CLI Reference

CommandPurposeReal Example Output
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;'Count active group registry16
wp eval '$g=acf_get_field_groups(["key"=>"group_service_structural_model"]); print_r(array_column(acf_get_fields($g[0]),"name"));'List service structural field namesservice_hero_group service_faq_items
wp eval '$id=1345; $rows=get_field("service_faq_items",$id); echo count((array)$rows) . PHP_EOL;'Read Repeater row count2
wp eval '$rows=get_field("landing_sections",1345); foreach((array)$rows as $row){echo $row["acf_fc_layout"] . PHP_EOL;}'List Flexible layout sequencehero_section testimonial_section
wp eval '$rows=get_field("landing_sections",1345); print_r($rows[0]);'Inspect first flexible row payloadArray ( [acf_fc_layout] => hero_section ... )
wp post meta get 1345 service_faq_itemsInspect raw stored structural dataa:2:{...}
wp eval '$unknown=[]; ... ; print_r($unknown);'Unknown-layout auditArray ( )
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro requirement

What's Next

tip

Revisit this lesson whenever a page builder request appears; it helps you decide whether to use Repeater, Flexible Content, or a simpler fixed model.