The forms manager: providers, lifecycle, and why Gravity Forms is the engine

Every MAM Suite app eventually needs to ask the user for something — a contact message, a profile edit, a listing submission, a booking. On a normal website, you’d just drop a form on a page and move on. A native app doesn’t have a page to drop it on: the form has to be described as data, shipped to the phone, rendered by the app’s own form renderer, submitted back over an API, validated on the server, and then turned into whatever the submission was actually for — an email, an SMS, a new post, a CRM record. The forms manager is the subsystem in mam-main that owns this entire round trip.

This article walks through the shape of that subsystem: what a “form” actually is inside MAM Suite, and why it’s built around pluggable providers instead of a single form engine. It covers how a submission travels from a tap on the phone back to a notification, and how a form gets pre-filled with the data the user is looking at. And it answers the question everyone asks: why Gravity Forms is the default engine, and what role it really plays.


Why an abstraction, and not just “use Gravity Forms”

It would have been simpler to just say “MAM forms are Gravity Forms forms” and leave it there. The forms manager deliberately doesn’t. Instead, it treats a form as an abstract thing — one that any provider must be able to answer three questions about:

  • What fields does this form have?
  • How do I render it for the mobile app?
  • How do I process a submission against it?

Anything that can answer those three questions counts as a valid form source. The mobile-app rendering pipeline and the submission pipeline never need to know where a form definition came from — they only ever talk to the abstraction. That’s what lets MAM Suite act as the “WooCommerce of mobile apps” for forms: the same app screen, the same submission API, and the same notification plumbing all work no matter whether the form was drawn in Gravity Forms, drawn in Ninja Forms, or generated entirely in PHP.

Three providers ship today:

Provider Where it lives Typical use
Gravity Forms The mam-gravity-forms-manager plugin, plus GF-aware code in mam-main/includes/forms-manager/ Most customer sites — contact forms, listing submissions, special offers, bookings
Ninja Forms The mam-ninja-forms-manager plugin The free-forms alternative for customers who build in Ninja Forms instead of Gravity Forms
Programmatic forms Plugin code that registers a form definition directly in PHP The user-profile screen, invitations, listing-claim forms, and other forms that have no business being hand-built in an admin UI

Ninja Forms and programmatic forms both plug in the same way: they hook the mam_form_manager_get_forms_from_plugins registration filter and hand back form definitions normalized to the GF-shaped array the transform expects. Nothing downstream of that filter cares that Gravity Forms exists. Additional providers can be added against that same extension surface — the contract stays generic; only the implementation behind the hooks changes.

So here’s the honest answer to “why Gravity Forms?”: Gravity Forms is the default engine, not the architecture. It’s the maturest, most widely deployed provider, and most no-code customers will build their forms there. But it sits behind an abstraction on purpose, so it stays replaceable — and so programmatic forms can live alongside it without the app ever noticing the difference.


What Gravity Forms actually does in the stack

Worth being precise here, because “Gravity Forms is the engine” is easy to misread as “Gravity Forms renders the app.”

Gravity Forms provides:

  • The form-building experience — drag-and-drop fields, conditional logic, the field schema, and the full GF feature set, all in WordPress admin.
  • The entry store — every app submission becomes a real GF entry. The mam-gravity-forms-manager bridge calls the Gravity Forms API to create an entry from app-submitted data, so a form submitted from the phone shows up in Forms → Entries exactly like one submitted on the website. There is no “submitted from app” badge; the two are indistinguishable on purpose.
  • GF’s own notifications — when an app submission is turned into a GF entry, the bridge asks Gravity Forms to fire whatever notifications that form has configured, so admins keep using the GF notification UI they already know.

What Gravity Forms does not do is render the form on the phone. The mobile app has its own native form renderer (vc_form_manager on iOS and its Android equivalent). What MAM Suite ships to the phone is a transformed, mobile-shaped description of the form’s fields — not GF’s HTML. The forms manager reads the GF schema, converts each field into a mobile field type the app knows how to draw — with the right keyboards, choice lists, and so on — caches that description, and hands it off to the phone-data pipeline.

That transform-and-cache step is the heart of the GF integration — and it’s also where most of the subtlety lives.


The two parallel hook namespaces (and why)

Read the code or the hook reference and you’ll notice the forms manager exposes its extension points under two different naming conventions:

  • A modern, provider-neutral namespace: mam_form_manager_*
  • A legacy, Gravity-Forms-flavored namespace: mam_gf_* and mam_for_gravity_forms_*

This is a historical artifact, not a design choice. When the forms manager first shipped, Gravity Forms was the only form integration, so that first generation of hooks got named after it. Once programmatic forms and the broader provider model arrived, a generic mam_form_manager_* namespace was added — but by then the original mam_gf_* names had already become public API that sibling and customer plugins depend on, so renaming them would have broken installations.

The practical guidance:

  • *For new code, prefer the generic `mam_formmanager` hooks.** They are the forward-looking contract.
  • Some operations still only exist on the legacy surface (for example, fetching a form definition by id). Where the generic namespace has no counterpart yet, the legacy hook is the only option.
  • Never rename a legacy hook. When in doubt: wrap, don’t rename. Closing the gap between the two namespaces is tracked as a post-launch cleanup, not something to do ad hoc.

If you’re not a developer, here’s the one thing to take away: the gf_ in a hook or option name does not mean a feature only works with Gravity Forms. Many of those names are path-agnostic; the prefix is just history.


The submission lifecycle

The most important thing the forms manager owns is the trip from a Submit tap all the way back to a result and any notifications it triggers. It runs in seven phases.

  1. AJAX entry. The app POSTs to a single submission endpoint. The handler resolves the form id and the current user from the request. Some forms — registration, contact — accept anonymous submissions, so the endpoint stays open to logged-out users too; in that case, the user resolves to id 0.

  2. Pre-process action. A mam_gf_processing_form action fires once per submission. This is where sibling plugins can watch a submission go by — for analytics, or for side effects that have nothing to do with the field data itself.

  3. Per-field processing. Each field flows through a standard field handler. Built-in types (text, email, phone, address, choice) are handled directly; custom field types extend here via a dynamic per-type filter, mam_form_manager_process_field_type_{type}. A custom field type needs two hooks to round-trip correctly — one at build time to render it for the app, one here at submit time to validate it. Miss either half, and the round trip breaks silently.

  4. Validation and the entry. Gravity Forms’ native validation runs (required, format, length), and on the GF path the submission becomes a real GF entry.

  5. Per-form submission hooks. A dynamic mam_for_gravity_forms_form_submitted_{form-id} filter (with mam_form_manager_form_submitted_{form-id} as its generic alias) fires for that specific form. This is the most common extension point for customer-specific logic — “do X when this form on this site is submitted” — because the form id is baked right into the hook name.

  6. Result envelope. A per-form result builder (mam_for_gravity_forms_form_result_form_{form-id}, then generic and _v2 passes) shapes the JSON envelope returned to the app. Typical keys are status, message, a redirect/refresh instruction, and any data the app should refresh after submitting.

  7. Notification dispatch. Finally, a mam_form_manager_send_notifications action fires. This is the single contract between the forms manager and the notifications system. Subscribers never email or text directly; they fire the notification dispatcher’s send action, and it routes to email, SMS, or push per the configured channels. On the Gravity Forms path, this is also where the bridge asks GF to send the form’s own notifications.

The rule that governs all of this: don’t shortcut the contracts. A submission handler that needs to notify someone should always go through mam_form_manager_send_notifications rather than calling the notification dispatcher directly — that’s what lets admin overrides intercept it, and keeps every channel dispatching consistently no matter which provider produced the submission.

Web submissions are a separate, parallel flow

Everything above is the app path. When a user fills out a Gravity Form on the WordPress website instead of in the app, a different flow runs entirely: the GF bridge listens for GF’s own after-submission event, maps the entry’s values into a custom post type, saves them as post meta, and fires a per-form web-result hook so a downstream plugin (Special Offers, GeoDirectory) can finish the job. The two flows are kept deliberately separate — the web path never calls the app result hooks, and the app path never calls the web result hook.


Prefill: opening a form already populated

The other half of “forms in an app” runs in the opposite direction: a user taps something they’re looking at, and a form opens already filled in with that item’s values. A booking form opens with the date and captains already picked; an edit form opens with the listing’s current values already in place.

The forms manager separates two things here:

  • The form payload — the cached, mobile-shaped form definition. It is shared across every place that opens that form.
  • The prefill values — the per-row, key-value pairs that should populate the form for this specific item. These are matched to fields by a populate_with_key slug.

Three established patterns handle wiring a tap to a pre-filled form. Each publishes different keys and targets a different part of the app’s UI, but they all share the same underlying idea: the form payload gets reused, and only the prefill dict changes per item.

Pattern What the user taps Where the prefill values live
Tabbar button A button across the bottom bar that opens a sheet of one or more forms A values dict on each form entry in the button’s source list
custom_form_array on a row An edit icon/button on a card or list row A values dict on each entry in the row’s form array
content_type: 'form' row tap The whole row — no button, the row itself is the affordance Top-level keys on the row, matched to field slugs

If you’re not a developer, here’s the point that matters: once a content feature is wired correctly, prefill just happens — the values follow whatever item you tapped. If you’re building a new feature, the pattern you choose follows the interaction you want (a dedicated edit button versus tap-the-whole-row), and the contract never changes: publish values keyed by the field slugs the form declares, and the app’s renderer matches them up by populate_with_key.


How the pieces connect

Put it all together, and a single MAM form touches several subsystems, each with one narrow job:

  • The provider (Gravity Forms or programmatic code) answers “what fields, how to render, how to process.”
  • The forms manager in mam-main discovers forms across providers, transforms and caches them into mobile shape, runs the submission lifecycle, and owns the extension hooks.
  • The mam-gravity-forms-manager bridge teaches the forms manager to read Gravity Forms as a source, creates GF entries from app submissions, and handles the separate web-submission-to-CPT flow.
  • The phone-data pipeline ships the cached form definitions to the app.
  • The notifications system receives the single mam_form_manager_send_notifications hand-off and fans it out to email, SMS, and push.

One closing caution about the caching layer between transformation and the phone, because it’s the most common source of surprise: form definitions are cached aggressively, since the app rebuilds its JSON on every load, and re-deriving every form from the GF API each time would be far too expensive. The cost of that speed is cache invalidation — if a setting that changes a form’s output gets written without invalidating the cache, admins will edit a form and the app will just keep showing the old version. Any feature that changes what a form renders has to invalidate the cache when it writes.

That trade-off — cache for speed, invalidate on change — sums up the whole subsystem’s philosophy: keep the app fast by shipping pre-built, provider-neutral form descriptions, and keep them correct by staying disciplined about the contracts at every seam.


Where to go next

  • Plugin: mam-gravity-forms-manager — the bridge in detail, including the app-submission and web-submission data flows.
  • Plugin: mam-contact-form — the simplest concrete example of a MAM form, with both a built-in handler path and a Gravity Forms path.
  • Forms architecture — providers and extensibility (mam-main reference) — the full hook surface and how to register a new provider.
  • Form submission lifecycle (mam-main reference) — the phase-by-phase trace with exact hook signatures.
  • Forms manager prefill paths (mam-main reference) — the three prefill patterns with their app-side resolver details.
Was this article helpful?
Contents

    Need Support?

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