Feed a custom post type from a web Gravity Forms submission

Metadata

Field Value
Article type How-to (Developer)
Plugin slug mam-gravity-forms-manager
Applies to plugin version 2.3+
Category Extending MAM Suite
Audience PHP developer
Estimated time 30 minutes
Depends on MAM Main, Gravity Forms, MAM Gravity Forms
Hooks used mam_gf_get_form_settings, mam_for_gravity_forms_web_form_result_form_{form_id}
Last verified 2026-06-03

Goal

You have a Gravity Form on your WordPress site that visitors submit from the web — not from inside the app. You want each submission to land as a post in a custom post type (CPT), with the submitted values stored as post meta, so that a downstream MAM consumer plugin (or the app itself) can read that content. After this how-to you will have a working “edit on web, read in app” path: a web submission creates or updates a CPT post, saves each field as meta keyed by your own slugs, and fires a per-form hook where you finish the job.

This is the same path mam-special-offers and mam-geodirectory use to back their flows, so once it works your content shape matches what those consumers already expect.


How the bridge works (read this first)

The bridge lives in mam-gravity-forms-manager (includes/submit-web-form.php, in mam_gf_submit_web_form::after_submission()). It listens to Gravity Forms’ gform_after_submission. On every web submission it:

  1. Gates on a subscriber. It checks has_filter( 'mam_for_gravity_forms_web_form_result_form_' . $form_id ). If nothing is registered for that form ID, the whole body short-circuits. No subscriber means no post is created — the hook is the trigger, not a post-processing afterthought.
  2. Reads your mapping. It calls apply_filters( 'mam_gf_get_form_settings', [] ) and finds the entry whose id option resolves to this submission’s form ID. That entry tells it which CPT to write to, which slugs build the title, which build the content, and the slug-to-GF-field mapping.
  3. Maps entry values out of the GF $entry array into a values array keyed by your slugs.
  4. Inserts or updates the post (wp_insert_post / wp_update_post), post_status publish by default, post_author the current user.
  5. Saves meta with update_post_meta( $post_id, $slug, $value ) for every mapped slug.
  6. Dispatches mam_for_gravity_forms_web_form_result_form_{form_id} with $form_id and $post_id.

The pivotal data structure is the mam_gf_get_form_settings entry. Get its shape right and the rest follows.


Prerequisites

  • The form is already wired into the app per Recipe: Add a Gravity Form to your app (the app side and the web side share the same mam_gf_get_form_settings mapping).
  • You can write and deploy a small custom plugin or a functions.php snippet — this is a developer task, not an admin one.
  • A target CPT already registered, or you are comfortable registering one. (The bridge does not register it for you; it only writes to it via post_type.)
  • The form’s Form ID (e.g. 42).
  • A list of the GF field IDs you want to capture, paired with the post-meta slugs you want them stored under.

Steps

1. Register your form mapping

Add a callback to mam_gf_get_form_settings. It returns an array of entries, each describing one form your code knows about. The entry’s data block is what the bridge consumes.

add_filter( 'mam_gf_get_form_settings', 'my_app_register_offers_form' );

function my_app_register_offers_form( $settings ) {
    $root = 'my_app_offers_';
    $slug = 'offers_form';

    $fields = [
        [ 'id' => 1, 'slug' => 'offer-title', 'title' => 'Offer Title', 'type' => 'text' ],
        [ 'id' => 2, 'slug' => 'description', 'title' => 'Description', 'type' => 'textarea' ],
        [ 'id' => 3, 'slug' => 'start-date',  'title' => 'Start Date',  'type' => 'date' ],
        [ 'id' => 5, 'slug' => 'post-id',     'title' => 'Offer ID',    'type' => 'hidden' ],
    ];

    $settings[] = [
        'category'    => 'general',
        'title'       => __( 'My App: Offers', 'my-app' ),
        'variable'    => 'none',
        'type'        => 'gravity_form',
        'id'          => $root . $slug,          // option key holding the GF form ID
        'environment' => 'backend',
        'data'        => [
            'cpt'          => 'offer',           // the CPT the bridge writes to
            'post_title'   => [ 'offer-title' ], // which slugs build post_title
            'post_content' => [ 'description' ], // which slugs build post_content
            'fields'       => $fields,
        ],
    ];

    return $settings;
}

Two things that trip people up:

  • id is an option key, not a form ID. The bridge reads get_option( $entry['id'] ) and compares the result to the incoming submission’s form ID. Whoever installs your plugin must set that option to the real GF form ID for this site.
  • Each field’s GF index is stored in its own option, keyed {id}_{slug} (e.g. my_app_offers_offers_form_offer-title). The bridge reads get_option( $root . $slug ) to find which GF field index a slug maps to on this install. The id value inside the fields array is your own reference number; the live mapping comes from the option.

2. Set the form ID and field indexes for this install

If you ship this as a feature plugin, expose an admin settings page so the site admin can pick the form and map fields. For a single site, set the options directly in an install routine:

update_option( 'my_app_offers_offers_form',             42 ); // GF form ID
update_option( 'my_app_offers_offers_form_offer-title', 1 );  // GF field IDs
update_option( 'my_app_offers_offers_form_description', 2 );
update_option( 'my_app_offers_offers_form_start-date',  3 );
update_option( 'my_app_offers_offers_form_post-id',     5 );

The values on the right are the actual Gravity Forms field IDs in the form on this site. If a slug’s option does not resolve to a field present in the submitted form, the bridge skips that slug.

3. Subscribe to the result hook (this is the trigger)

Add a callback to the dynamic hook mam_for_gravity_forms_web_form_result_form_{form_id}, substituting your real form ID. Registering any callback at any priority is what flips the gate in step 1 and enables the whole post-creation flow.

add_filter( 'mam_for_gravity_forms_web_form_result_form_42', 'my_app_offer_submitted', 10, 2 );

function my_app_offer_submitted( $form_id, $post_id ) {
    // By now the post exists and meta is saved by mam-gravity-forms-manager.
    if ( ! get_post( $post_id ) ) {
        return $form_id;
    }

    // Read a value back out of the meta the bridge just stored.
    $title = get_post_meta( $post_id, 'offer-title', true );

    // Do the work that depends on the post existing: set status, geocode,
    // send a custom email, mark the user as having submitted, etc.
    do_action( 'my_app_offer_submitted', $post_id, $title );

    return $form_id;
}

The hook is declared with apply_filters() even though it is used for side effects. The plugin ignores your return value, but you must still return something — by convention return $form_id — so that other subscribers in the chain receive a value rather than null. Treat it as an action you are forced to spell add_filter.

4. (Optional) Let the form update an existing post

If a submission should edit a post that already exists — an admin re-edit flow, for example — give the bridge a post ID. Two ways:

  • Add a hidden GF field mapped to the post-id slug. The bridge reads the post ID out of the mapped value.
  • Serve the form with ?post_id=123 on the URL. The bridge runs absint() on $_REQUEST['post_id'] and uses it.

Note the precedence: when $_REQUEST['post_id'] is present, it wins over the mapped post-id field.

Supplying a post ID only switches the bridge from wp_insert_post() to wp_update_post() when the submitter is authorized to edit that post. The bridge runs an ownership-and-type check (mam_user_can_edit_post_of_type, filterable via mam_gf_web_form_can_update_post) before overwriting an existing post; if the check fails it inserts a new owned post instead of overwriting someone else’s. So an admin re-edit updates in place, but an unauthorized or guest submission with a stray ?post_id= will not clobber the target.

5. Verify

  1. Submit the GF form on the WordPress site (not in the app).
  2. Open the CPT list in WP admin and confirm a new post appears, titled from your post_title slugs and bodied from your post_content slugs.
  3. Open the post’s Custom Fields panel and confirm each mapped slug is stored as a meta key with the submitted value.
  4. Confirm your result callback ran — check its side effect, or drop a temporary mamdebug line.
  5. Re-submit with ?post_id={existing-id} on the URL and confirm the existing post updates instead of a new one being created.

Common gotchas

  • No subscriber, no post. The most common surprise: the result hook is the trigger. If posts are not being created, first confirm your add_filter() for mam_for_gravity_forms_web_form_result_form_{form_id} actually runs and uses the correct form ID.
  • id is an option key, not a form ID. This is the single biggest confusion when reading consumer code. Search for the update_option( $root . $slug, ... ) calls to see how the form ID and field indexes get populated.
  • mam_gf_get_form_settings is read twice per submission. The bridge applies it once for the field mapping and again for the CPT/title/content composition. Keep your callback idempotent and cheap.
  • update_post_meta runs for every slug, including post-id. The post ID gets stored in its own meta too. Harmless, but worth knowing if you grep for orphan meta.
  • App submissions do not trigger this flow. They go through mam_form_manager_send_notifications, a different code path. To react to both, subscribe to each.
  • Default status is publish and author is the current user. For an unauthenticated submission the author is 0. If your CPT should not auto-publish, set post_status to pending inside your callback. Decide explicitly what guest submissions should do.
  • Do not rely on $entry in the callback. The signature is ($form_id, $post_id) — no entry array. Read values back via get_post_meta(). The raw entry is also in the $mam_form_entry global, but leaning on a global is brittle; prefer the meta path.

  • Recipe: Route a web submission to a custom post type — the longer reference recipe this how-to condenses.
  • Hook: mam_for_gravity_forms_web_form_result_form_{form_id} — full reference for the result hook, with email/geocode/status examples.
  • Hook: mam_gf_get_form_settings — the provider hook the bridge reads to learn how to map an entry to a post.
  • Recipe: Add a Gravity Form to your app — the app-side counterpart that shares the same mapping.

What’s next

Once submissions land in your CPT, point a MAM consumer at that content: register a content class that reads the CPT and its meta (Build your first custom content class), or inject the meta into the mobile JSON payload so the app can render it. If different forms should share one handler, register the same callback against each form’s hook and branch on $form_id.


Verification

This draft was grounded against:

  • Plugin: mam-gravity-forms-manager v2.3
  • Source: mam-gravity-forms-manager/includes/submit-web-form.php (mam_gf_submit_web_form::after_submission())
  • Reference docs: 04-recipe-route-web-submission.md, 05-hook-mam-for-gravity-forms-web-form-result-form-id.md
  • Reference consumers: mam-special-offers, mam-geodirectory

Re-verify whenever the order of post creation, meta save, and result dispatch in after_submission() changes; the mam_gf_get_form_settings entry shape (id, data.cpt, data.post_title, data.post_content, data.fields) changes; the $_REQUEST['post_id'] precedence over the mapped post-id field changes; or the dispatcher moves from apply_filters() to do_action() (planned, TD-GF-005).

Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!