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-mainactive. (mam-mainis 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_pluginsstep. - 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
typevalues the renderer accepts, and howsettingsis 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.phpgates onhas_filter()for exactly this name. Older notes mention asubmit_actioncallback 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
- Discovery:
var_dump( apply_filters( 'mam_form_manager_get_forms_from_plugins', array() ) )should include your form definition withis_custom => true. - In the app: the form appears wherever you’ve surfaced it and renders your fields with the right input types.
- 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. - 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 |
Related
- 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 legacymam_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.
