Gravity Forms can create WordPress posts on submission via the Post Fields module, but Post Fields only map to standard WordPress post fields (title, content, excerpt, custom taxonomies) and not to ACF fields. To populate ACF fields from a Gravity Forms submission, the canonical pattern is the gform_after_submission hook plus update_field calls. The dedicated Gravity Forms + ACF add-on plugin is the alternative when you want a UI-driven mapping. Here is each.
Jump to:
- Why Post Fields alone do not cover ACF
- The gform_after_submission hook pattern
- Mapping form fields to ACF fields
- Handling complex ACF field types from a form
- The dedicated Gravity Forms + ACF plugin path
- Validation before save
- User permissions and post status
Why Post Fields alone do not cover ACF
Gravity Forms has a built-in "Post Fields" group that maps form inputs directly to standard WordPress post fields:
- Title
- Body (post_content)
- Excerpt
- Tags
- Category
- Custom Field (writes to
wp_postmetaviaupdate_post_meta)
The Custom Field option does write to wp_postmeta, which is where ACF data lives, BUT it writes only the raw value and not the ACF field-key reference row. The next time ACF reads the field, it may not recognize the value as ACF-managed and may behave unexpectedly (the editor UI shows the value but update_field calls behave oddly).
Using update_field after the form submission is the canonical correct path. The hook is gform_after_submission.
The gform_after_submission hook pattern
gform_after_submission fires after Gravity Forms creates the entry and the post (if Post Fields are configured). The callback receives the entry and the form:
add_action( 'gform_after_submission_5', 'populate_acf_from_form', 10, 2 );
// "_5" targets form ID 5; use 'gform_after_submission' to apply to all forms
function populate_acf_from_form( $entry, $form ) {
// Get the post ID created by the form (if Post Fields are used)
$post_id = rgar( $entry, 'post_id' );
if ( ! $post_id ) {
// No post was created by Post Fields; nothing to update
return;
}
// Map form field IDs to ACF field names
update_field( 'phone', rgar( $entry, '3' ), $post_id ); // form field ID 3
update_field( 'company', rgar( $entry, '4' ), $post_id );
update_field( 'industry', rgar( $entry, '5' ), $post_id );
update_field( 'is_subscriber', rgar( $entry, '6' ) === 'yes', $post_id );
}rgar() is Gravity Forms' helper for "read get array": it safely reads from the entry array and returns empty string if the key does not exist.
The form field IDs (3, 4, 5, 6 in the example) are visible in the Gravity Forms form editor next to each field. Use these exact IDs in the mapping.
Mapping form fields to ACF fields
For maintainability, define the mapping in a configuration array rather than inline:
add_action( 'gform_after_submission_5', function ( $entry, $form ) {
$post_id = rgar( $entry, 'post_id' );
if ( ! $post_id ) return;
$mapping = [
// form_field_id => acf_field_name
'3' => 'phone',
'4' => 'company',
'5' => 'industry',
'6' => 'is_subscriber',
'7' => 'website_url',
'8' => 'years_in_business',
];
foreach ( $mapping as $form_field_id => $acf_field_name ) {
$value = rgar( $entry, $form_field_id );
update_field( $acf_field_name, $value, $post_id );
}
}, 10, 2 );When the form changes (new field added, existing field renumbered), update the mapping array. The rest of the logic does not need to change.
Handling complex ACF field types from a form
Date fields: Gravity Forms returns the date string in the form's configured format. ACF expects Ymd for storage. Convert:
$date_string = rgar( $entry, '9' ); // e.g., "12/25/2026"
$date_obj = DateTime::createFromFormat( 'm/d/Y', $date_string );
if ( $date_obj ) {
update_field( 'event_date', $date_obj->format( 'Ymd' ), $post_id );
}Checkbox fields: Gravity Forms returns checkbox values as multiple entries with sub-keys (6.1, 6.2, 6.3 for the three checkbox options). Collect them into an array:
$checkboxes = [];
foreach ( [ '6.1', '6.2', '6.3' ] as $sub_key ) {
$val = rgar( $entry, $sub_key );
if ( ! empty( $val ) ) $checkboxes[] = $val;
}
update_field( 'tags', $checkboxes, $post_id );File upload fields: Gravity Forms returns the uploaded file URL. ACF Image fields expect an attachment ID. Convert via attachment_url_to_postid or by sideloading the file:
$file_url = rgar( $entry, '10' );
if ( $file_url ) {
$attachment_id = attachment_url_to_postid( $file_url );
if ( $attachment_id ) {
update_field( 'featured_image', $attachment_id, $post_id );
}
}If the GF upload writes the file outside the media library, you need to sideload it first with media_sideload_image or media_handle_sideload and then use the resulting attachment ID.
Repeater fields: Gravity Forms does not natively support Repeater-shaped submissions. For multi-row data, either submit individual rows via separate form submissions or use a different form pattern. Or look at Sending Gravity Forms Data Into ACF Repeater Fields.
The dedicated Gravity Forms + ACF plugin path
For sites where the form-to-ACF mapping changes frequently and you want a UI-driven approach, the dedicated Gravity Forms + ACF add-on plugins handle the mapping via the Gravity Forms admin. The most-used one is "ACF Frontend Form" (third-party); the official Gravity Forms add-ons handle some ACF field types but coverage varies.
The trade-off:
- Hook-based mapping (above): Code-managed, version-controlled, explicit, supports any ACF field type.
- Plugin-based mapping: UI-managed, easier for non-developers, may not cover every ACF field type.
For agency work I prefer the hook pattern because it lives in code and travels with deploys. For sites where the marketing team needs to add new fields without a developer, the plugin path makes sense.
Validation before save
gform_after_submission fires after Gravity Forms has validated the form and saved the entry. If you need to abort the post update based on a condition the form does not check, do it in the callback:
add_action( 'gform_after_submission_5', function ( $entry, $form ) {
$post_id = rgar( $entry, 'post_id' );
if ( ! $post_id ) return;
// Custom validation: company field must contain a known whitelist
$allowed = [ 'Acme', 'Globex', 'Initech' ];
$company = rgar( $entry, '4' );
if ( ! in_array( $company, $allowed, true ) ) {
// Mark post as pending review instead of populating ACF
wp_update_post( [ 'ID' => $post_id, 'post_status' => 'pending' ] );
return;
}
// Normal mapping
update_field( 'company', $company, $post_id );
}, 10, 2 );For pre-save validation (preventing the form from submitting at all), use gform_validation instead.
User permissions and post status
Gravity Forms creates posts as the currently-logged-in user (or as a configured user if the user is not logged in). For public-facing forms, posts often get created with post_status = 'pending' so an editor can review before publish.
If your ACF population should only run for published posts:
add_action( 'gform_after_submission_5', function ( $entry, $form ) {
$post_id = rgar( $entry, 'post_id' );
if ( ! $post_id || get_post_status( $post_id ) !== 'publish' ) return;
// ACF mapping only for published posts
}, 10, 2 );Or, more commonly, populate ACF regardless of status (so the data is there when the editor reviews and publishes).
For the broader pattern of Gravity Forms + ACF integration including Repeater data, see Sending Gravity Forms Data Into ACF Repeater Fields. For the agency operations side (lead-capture forms, client onboarding forms), see How to Start a WordPress Agency in 2026 Without Burning Yourself Out.
Sources
Authoritative references this article was fact-checked against.
- Gravity Forms developer hooks (Gravity Forms docs)docs.gravityforms.com
- update_field() function reference (Advanced Custom Fields docs)advancedcustomfields.com





