Register a custom (non-Gravity-Forms) form with the Forms Manager

Goal

Surface a form in a MAM app whose fields come from your plugin’s data — not from Gravity Forms — while still reusing the app’s built-in form renderer and submission lifecycle. You return a form definition in the mobile-renderable shape, and the Forms Manager treats it like any other form: it shows up in the app, the user fills it out, and your handler receives the submission.

Use this when a form isn’t worth building in Gravity Forms: a one-field caregiver-invite acceptance, an IAP receipt confirmation, an in-app rating prompt, a listing-claim form, or any surface where your plugin owns the field contract end to end.

GF-backed vs. custom (programmatic) forms. If admins need to drag-and-drop fields in the WordPress UI, or you want Gravity Forms features (conditional logic, calculations, the GF entries grid), build a GF-backed form instead and register it through mam_gf_get_form_settings. The end-to-end cookbook covers that path. This how-to is for the code-owned path where the fields are fixed by your plugin.


Prerequisites

  • A sibling plugin loaded inside MAM Suite with mam-main active. (mam-main is the host that owns the Forms Manager; if it isn’t active your hooks won’t fire.)
  • PHP developer access to that plugin’s code. There is no admin UI for this — registration is done in PHP.
  • Familiarity with how a form reaches the app: the phone-data pipeline asks “what forms exist,” every provider appends its forms, and the result is cached. Custom forms join that list at the mam_form_manager_get_forms_from_plugins step.
  • A place to persist submissions (your own table, post meta, an option, an external API). Custom forms do not write to Gravity Forms entries — persistence is your handler’s job.

Steps

1. Register your form on mam_form_manager_get_forms_from_plugins

This filter is the primary registration hook for non-Gravity-Forms forms. mam-main fires it while assembling the full list of known forms (get_all_gravity_forms() in includes/forms-manager/mam-gravity-forms-class.php), so any form you append here is discovered alongside GF forms.

Append a form definition in the mobile-renderable shape. Custom forms skip the Gravity Forms transformation chain, so your fields array has to already match the shape the app expects — there’s no GF form behind it to transform.

add_filter( 'mam_form_manager_get_forms_from_plugins',
    function ( array $forms ): array {

        $forms[] = array(
            'id'          => 'mam-my-plugin-rating_prompt', // globally unique
            'title'       => 'Rate this app',
            'fields'      => array(
                array(
                    'id'       => '1',
                    'type'     => 'rating',
                    'label'    => 'Stars',
                    'required' => true,
                    'max'      => 5,
                ),
                array(
                    'id'       => '2',
                    'type'     => 'textarea',
                    'label'    => 'Optional feedback',
                    'required' => false,
                ),
            ),
            'settings'    => array(
                'submit_label' => 'Submit rating',
            ),
            'is_custom'   => true, // signals "this isn't a GF form"
        );

        return $forms;
    }
);

Register on plugins_loaded (or earlier) so your filter is in the pipeline before any phone-data pull runs. A declaration that lands too late won’t make it into the cached form list.

Form definition fields:

Field Purpose
id Unique identifier, used as the dispatch key for submissions. Convention: <plugin-slug>-<event-name>. Must be globally unique across both GF and custom forms.
title Display title shown in the app.
fields Field definitions already in the mobile render shape (id, type, label, required, plus type-specific keys like max).
settings Per-form presentation — submit-button copy, colors.
is_custom Convention marker noting the definition is non-GF in origin. mam-main’s discovery and submit paths dispatch off the per-form mam_form_manager_form_submitted_<id> filter (step 2), not this flag — include it for clarity, but the pipeline does not read it to gate GF processing.

Field-shape details (the exact set of type values the renderer accepts, and how settings is structured) are part of the frozen mobile contract. If you’re unsure of an exact key, copy the shape an existing form already publishes rather than inventing one.

2. Wire a submission handler keyed to your form id

When the app submits, mam-main’s mam_gf_submit_app_form::submit_form() (includes/forms-manager/submit-app-form.php) checks for a per-form submission filter named after your form id:

has_filter( 'mam_form_manager_form_submitted_' . $form_id )

If that filter exists, the submission is routed to it. Add a callback for your form’s id:

add_filter(
    'mam_form_manager_form_submitted_mam-my-plugin-rating_prompt',
    function ( $response, $values, $raw_formdata ) {

        $stars = (int) ( $values['1']['value'] ?? 0 );
        $body  = wp_strip_all_tags( $values['2']['value'] ?? '' );

        if ( $stars < 1 || $stars > 5 ) {
            return array(
                'status'  => 'error',
                'type'    => 'error',
                'message' => 'Please choose a rating.',
            );
        }

        // Persist however your plugin sees fit — your table, post meta, an API.
        my_plugin_persist_rating( get_current_user_id(), $stars, $body );

        return array(
            'status'             => 'success',
            'type'               => 'refresh_return',
            'message'            => 'Thanks for the feedback!',
            'process_normal'     => false, // we fully replaced GF's pipeline
            'send_notifications' => false, // we'll dispatch our own, see step 4
        );
    },
    10, 3
);

The handler receives the in-progress response array, the parsed $values, and the raw submitted form data. Read your values out of $values keyed by field id (or by slug, depending on how the values were assembled upstream — verify against what your registration in step 1 produced).

The mam_form_manager_form_submitted_<id> filter is the path the submit pipeline actually keys off — submit-app-form.php gates on has_filter() for exactly this name. Older notes mention a submit_action callback name on the form definition, but mam-main’s submit code does not read it. Wire your handler with the dynamic per-form filter.

3. Return the result envelope the app expects

The app waits on a specific response shape. Return the full envelope — partial returns can leave the app hanging waiting on a status field.

Key Required Notes
status yes 'success' or 'error'. Drives whether the app shows the confirmation or the error.
type yes 'refresh_return' dismisses the form and refreshes the previous screen; 'error' and 'text' are the other values shipping handlers return.
message yes Text shown to the user.
process_normal recommended false for app-bound custom forms — your handler is fully replacing any default confirmation pipeline.
send_notifications recommended false if you dispatch your own notifications (step 4); true to let the generic notification dispatcher fire.

4. (Optional) Send notifications through the generic dispatcher

If a submission should trigger an email, push, or SMS, do not call the underlying notification sender directly from form-handling code. Go through the Forms Manager’s notification action so dispatch stays uniform across all providers and admin overrides can intercept:

do_action( 'mam_form_manager_send_notifications', $entry, $form );

mam-main fires this same action inside submit-app-form.php after a successful submission, then routes to the configured channels. Firing it yourself (rather than the channel-specific sender) keeps your custom form consistent with GF-backed forms.

5. Invalidate the cache when the form definition changes

Your filter (mam_form_manager_get_forms_from_plugins) re-runs on every phone-data assembly, so a form supplied purely through the filter is rebuilt on each pull and needs no manual invalidation. The assembled form_data block is only persisted when JSON-element caching is enabled (mam_main_allow_cache_json_elements); in that mode it is re-primed on mam_initial_phone_data via setup_forms_for_caching(). If that caching is on and a stale definition survives, bust the app’s cached JSON so the next pull reassembles the list.


Verify it

  1. Discovery: var_dump( apply_filters( 'mam_form_manager_get_forms_from_plugins', array() ) ) should include your form definition with is_custom => true.
  2. In the app: the form appears wherever you’ve surfaced it and renders your fields with the right input types.
  3. Submission wiring: has_filter( 'mam_form_manager_form_submitted_' . '<your-form-id>' ) returns true. If you built the hook name from an option, the option must be set before your plugin’s init runs.
  4. Round trip: submitting persists the data and returns a full envelope; the app shows your confirmation message and refreshes.

When to use this vs. a GF-backed form

Use a custom form Use a GF-backed form
One or two fixed fields Many fields, or admins must edit them
Fields are owned by your plugin’s code Admins drag-and-drop in the WP UI
You persist to your own storage Submissions store in GF entries
You don’t need GF logic/calculations/addons You want the full Gravity Forms feature set

  • Hook reference: mam_form_manager_get_forms_from_plugins — the registration hook in detail, including the form shape and gotchas.
  • Forms architecture — providers and extensibility — the pluggable provider model, the modern mam_form_manager_* surface, and the legacy mam_gf_* namespace.
  • Forms Manager end-to-end cookbook — the GF-backed path walked one form at a time (declare, configure, cache, inject, surface, handle).
  • Plugin: mam-contact-form — a shipping example that supports both a built-in handler and a GF-backed path.

What’s next

Once your form registers and submits cleanly, decide how it reaches the user — a tab-bar button, an inline row edit button, or a tap-the-row cell. The surfacing patterns (and their prefill paths) are covered in the Forms Manager end-to-end cookbook. If your plugin introduces a field type the renderer doesn’t know, register a per-type processor on mam_form_manager_process_field_type_<type> so the app can render it.

Was this article helpful?
Contents

    Need Support?

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