TechEarl

Sending Gravity Forms Data Into ACF Repeater Fields

Gravity Forms does not natively submit Repeater-shaped data, but three patterns handle the common cases: append-per-submission, single submission with grouped sections, and a custom JSON-encoded field. Here is each with code.

Ishan Karunaratne⏱️ 5 min readUpdated
Share thisCopied
Gravity Forms does not natively submit Repeater-shaped data. Three patterns: append-per-submission, grouped sections, JSON-encoded field. Real code.

Gravity Forms does not have a native concept of "multiple-row" submissions that maps onto an ACF Repeater. Three patterns cover the common cases: append a new Repeater row per form submission (best for "add a testimonial" forms), submit a single form with multiple grouped sections (limited but works for fixed row counts), or use a single text field encoded as JSON and parse it server-side (most flexible). Each has a use case; here is each with code.

Jump to:

The fundamental mismatch

Gravity Forms is a flat-form builder. Each form has a fixed set of fields; each submission produces one entry with values for those fields. There is no built-in "add another row" concept where the user dynamically adds N copies of the same group of fields.

ACF Repeater is exactly the opposite: a dynamic array of rows where each row has sub-fields.

The three patterns below bridge the gap, each with different trade-offs around UX, technical complexity, and editor-side behavior.

Pattern 1: append-per-submission

The simplest case. Each form submission appends one row to an ACF Repeater on a target post. Useful for "add a testimonial" or "submit your event" forms where users contribute one item at a time.

php
add_action( 'gform_after_submission_7', function ( $entry, $form ) {
    // Target post that holds the Repeater
    $target_post_id = 42; // or computed from the form context

    // Build the new row
    $new_row = [
        'author_name'  => rgar( $entry, '1' ),
        'author_email' => rgar( $entry, '2' ),
        'testimonial'  => rgar( $entry, '3' ),
        'rating'       => (int) rgar( $entry, '4' ),
        'submitted_at' => current_time( 'mysql' ),
    ];

    // Append using ACF's add_row helper
    add_row( 'testimonials', $new_row, $target_post_id );
}, 10, 2 );

add_row is ACF's helper that appends without needing to read the existing array first. Cleaner than the read-then-write pattern when you are only appending.

For moderation workflows, mark new rows as is_approved = false and require an editor to flip the flag before they appear in the front-end render:

php
$new_row['is_approved'] = false;
add_row( 'testimonials', $new_row, $target_post_id );

Pattern 2: grouped sections with fixed row count

When the form has a fixed N-row Repeater (e.g., "add up to 3 references"), Gravity Forms Section breaks can visually group the fields and you map them as N independent rows:

The form has fields 1-12 grouped into 4 sections of 3 fields each: name, email, company. The hook reads them as 4 rows:

php
add_action( 'gform_after_submission_8', function ( $entry, $form ) {
    $post_id = rgar( $entry, 'post_id' );
    if ( ! $post_id ) return;

    $rows = [];
    for ( $i = 0; $i < 4; $i++ ) {
        $base = ( $i * 3 ) + 1; // form field IDs 1, 4, 7, 10
        $name = rgar( $entry, (string) $base );
        $email = rgar( $entry, (string) ( $base + 1 ) );
        $company = rgar( $entry, (string) ( $base + 2 ) );

        // Only include rows that have at least a name
        if ( ! empty( $name ) ) {
            $rows[] = [
                'name' => $name,
                'email' => $email,
                'company' => $company,
            ];
        }
    }

    update_field( 'references', $rows, $post_id );
}, 10, 2 );

Limitations:

  • The maximum row count is fixed in the form. Users cannot add more than N.
  • The form has N times the field count, which is a lot of fields in the editor.
  • Empty rows are filtered server-side; the form itself does not enforce contiguity.

This pattern works for "up to 3 references" but does not scale to "up to 50 items."

Pattern 3: JSON-encoded text field

For genuinely dynamic row counts, the cleanest pattern is a single text/hidden field on the form that holds a JSON-encoded array of rows, populated by JavaScript on the front-end:

The form has one hidden field (form field ID 5) holding JSON. The form's front-end JavaScript adds a "+ add row" UI and stringifies the rows into the hidden field on submission.

The server-side hook decodes and writes to ACF:

php
add_action( 'gform_after_submission_9', function ( $entry, $form ) {
    $post_id = rgar( $entry, 'post_id' );
    if ( ! $post_id ) return;

    $json = rgar( $entry, '5' );
    $rows = json_decode( $json, true );

    if ( ! is_array( $rows ) ) return;

    // Sanitize each row's fields
    $clean_rows = [];
    foreach ( $rows as $row ) {
        $clean_rows[] = [
            'item_name' => sanitize_text_field( $row['item_name'] ?? '' ),
            'quantity'  => (int) ( $row['quantity'] ?? 0 ),
            'notes'     => sanitize_textarea_field( $row['notes'] ?? '' ),
        ];
    }

    update_field( 'items', $clean_rows, $post_id );
}, 10, 2 );

The front-end JavaScript (added via a custom theme script):

javascript
// Add row button + serialize on submit
document.querySelector('.gform_wrapper').addEventListener('submit', function () {
    const rows = Array.from(document.querySelectorAll('.dynamic-row')).map(row => ({
        item_name: row.querySelector('[name="item_name"]').value,
        quantity: row.querySelector('[name="quantity"]').value,
        notes: row.querySelector('[name="notes"]').value,
    }));
    document.querySelector('input[name="input_5"]').value = JSON.stringify(rows);
});

This pattern is the most flexible but requires custom JavaScript and styling. Worth it when row counts genuinely vary and the form is critical UX.

Validation for Repeater-bound submissions

For all three patterns, server-side validation matters because Gravity Forms cannot natively validate Repeater shapes:

php
add_action( 'gform_after_submission_9', function ( $entry, $form ) {
    $rows = json_decode( rgar( $entry, '5' ), true );

    // Validate before writing
    if ( ! is_array( $rows ) || count( $rows ) < 1 ) {
        // Mark the entry for manual review
        GFAPI::update_entry_property( $entry['id'], 'status', 'spam' );
        return;
    }

    if ( count( $rows ) > 50 ) {
        // Too many rows; possibly malicious
        GFAPI::update_entry_property( $entry['id'], 'status', 'spam' );
        return;
    }

    // Validate each row's structure
    foreach ( $rows as $row ) {
        if ( empty( $row['item_name'] ) || mb_strlen( $row['item_name'] ) > 200 ) {
            GFAPI::update_entry_property( $entry['id'], 'status', 'spam' );
            return;
        }
    }

    // OK to write
    update_field( 'items', $rows, $post_id );
}, 10, 2 );

The spam flag in Gravity Forms hides the entry from the default entry list. Manual review can recover legitimate-but-flagged entries.

The third-party "Repeater field" plugins

Several third-party Gravity Forms add-ons add a UI-driven "Repeater" or "Field Group" concept (Gravity Wiz's Gravity Perks suite has one, among others). They simplify the front-end side (no custom JavaScript needed) and integrate with the form editor.

The trade-off:

  • Plugin path: UI-driven, easier to maintain for non-developers, additional plugin cost and dependency.
  • Custom JSON path: Code-managed, no plugin dependency, requires custom JavaScript.

For long-lived agency client sites, I default to the custom JSON path because it has no third-party dependency to maintain. For sites where the client wants to manage forms themselves, the plugin path can be worth the cost.

When to use a different tool entirely

If you find yourself building "form with dynamic rows that creates a post with a Repeater" frequently, consider that Gravity Forms might not be the right tool. The alternatives:

  • Standalone post submission via the WordPress REST API. Full control, native Repeater support, requires building the UI from scratch.
  • A custom front-end form built with React/Vue. Same flexibility, requires the framework.
  • WPForms or Fluent Forms for cases where their dynamic-field handling is better fit.
  • A separate post type for each "row" with Relationship back to the parent, submitted as individual form posts. Often cleaner architecturally than a Repeater.

For the broader pattern of agency-scale Gravity Forms + ACF integration (especially for client onboarding and lead-capture workflows), see How to Populate ACF Fields from Gravity Forms. For the related "when to use a separate post type vs Repeater" decision, see Why Deeply Nested ACF Flexible Content Layouts Become a Maintenance Nightmare.

Sources

Authoritative references this article was fact-checked against.

TagsWordPressACFGravity FormsRepeater

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Count Rows in an ACF Repeater Field

Counting ACF Repeater rows is three short patterns: count() on the raw field, get_field_count() inside a loop, and a faster meta-only count that skips loading the rows. Each has its right use case.

How to Set Default Values in ACF Select Fields

ACF Select fields have a Default Value setting in the field group editor that handles the simple case. For dynamic defaults (computed from another field, role-based, or per-post-type), the acf/load_value filter is the right tool.