Form Builder Tracking
Popular form builders all have AJAX-based submission flows that bypass GTM’s built-in Form Submission trigger. Each one exposes a different hook for detecting successful submissions — some through DOM events, others through postMessage or JavaScript SDK callbacks.
This guide covers the six most common builders with tested, production-ready patterns for each.
Gravity Forms (WordPress)
Section titled “Gravity Forms (WordPress)”Gravity Forms fires a JavaScript event on the DOM when each form successfully submits or confirmation content loads.
For standard AJAX submissions:
<!-- Custom HTML tag, fires on DOM Ready --><script>jQuery(document).on('gform_confirmation_loaded', function(event, formId) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'gravity_forms', form_id: formId, form_name: 'GF Form ' + formId // Enhance: use a lookup table to map IDs to names });});</script>For non-AJAX (page-redirect) submissions, use a page-view-based approach: fire a custom event when the user lands on the confirmation page URL:
- Page View trigger with condition: Page URL contains
?gf_confirmation=
For multi-page forms, track each page advance:
<script>jQuery(document).on('gform_page_loaded', function(event, formId, currentPage) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_step_complete', form_builder: 'gravity_forms', form_id: formId, form_step: currentPage });});</script>Contact Form 7 (WordPress)
Section titled “Contact Form 7 (WordPress)”Contact Form 7 fires the wpcf7mailsent custom DOM event after a successful mail send. This event bubbles to the document, making it easy to catch from a Custom HTML tag:
<!-- Custom HTML tag, fires on DOM Ready --><script>document.addEventListener('wpcf7mailsent', function(event) { var detail = event.detail; window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'contact_form_7', form_id: detail.contactFormId, form_unit_tag: detail.unitTag // Unique ID for this form instance on the page });});</script>CF7 also fires wpcf7invalid (validation failure) and wpcf7mailfailed (mail sending error). Track validation failures for form UX analysis:
<script>document.addEventListener('wpcf7invalid', function(event) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_validation_error', form_builder: 'contact_form_7', form_id: event.detail.contactFormId });});</script>HubSpot Forms
Section titled “HubSpot Forms”HubSpot forms are loaded in an iframe from forms.hubspot.com. Cross-origin iframe submissions are communicated to the parent page via window.postMessage.
<!-- Custom HTML tag, fires on DOM Ready --><script>window.addEventListener('message', function(event) { // Only process messages from HubSpot if (event.origin !== 'https://forms.hubspot.com') return;
var data; try { data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; } catch (e) { return; }
// HubSpot sends type 'hsFormCallback' with eventName 'onFormSubmitted' if (data.type === 'hsFormCallback' && data.eventName === 'onFormSubmitted') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'hubspot', form_id: data.id, form_target: data.target }); }
// Also track form ready (displayed to user) if (data.type === 'hsFormCallback' && data.eventName === 'onFormReady') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_view', form_builder: 'hubspot', form_id: data.id }); }});</script>Typeform
Section titled “Typeform”Typeform’s embeds support an SDK for event listening. The approach differs between embedded forms and popup forms.
Installed via Typeform’s embed script:
<!-- Assuming Typeform Embed SDK is loaded --><script>// For embedded typeforms (in an iframe on the page)var tf = window.tf;if (tf) { tf.createWidget({ id: 'your-typeform-id', // Your form ID onSubmit: function(payload) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'typeform', form_id: payload.formId, response_id: payload.responseId }); } });}</script>For popup typeforms (opened via a button):
<script>// After the Typeform popup script has loadeddocument.querySelectorAll('[data-tf-popup]').forEach(function(button) { var formId = button.dataset.tfPopup; button.addEventListener('data-tf-submit', function(event) { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'typeform', form_id: formId }); });});</script>If you use the Typeform postMessage API directly:
<script>window.addEventListener('message', function(event) { if (event.origin !== 'https://form.typeform.com') return;
var data; try { data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; } catch (e) { return; }
if (data && data.type === 'form-submit') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'typeform', form_id: data.formId || 'unknown' }); }});</script>Webflow Forms
Section titled “Webflow Forms”Webflow fires a custom DOM event on the form element after successful submission:
<!-- Custom HTML tag, fires on DOM Ready --><script>document.addEventListener('submit', function(event) { var form = event.target; if (!form || form.tagName !== 'FORM') return;
// Webflow form detection — look for Webflow's own submission handler // Webflow adds a 'w-form' class to its form containers var formBlock = form.closest('.w-form'); if (!formBlock) return;
// Wait for Webflow to process the submission and show success state var successEl = formBlock.querySelector('.w-form-done'); if (!successEl) return;
// MutationObserver to detect when the success message appears var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.target.style.display !== 'none') { observer.disconnect(); window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'form_submission', form_builder: 'webflow', form_name: form.getAttribute('data-name') || form.id || 'unknown' }); } }); });
observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });});</script>A simpler approach if Webflow is configured to redirect to a thank-you page: use a Page View trigger on the thank-you page URL.
Calendly
Section titled “Calendly”Calendly communicates booking events via postMessage from its iframe embed:
<!-- Custom HTML tag, fires on DOM Ready --><script>window.addEventListener('message', function(event) { if (event.origin !== 'https://calendly.com') return;
var data; try { data = event.data; } catch (e) { return; }
if (!data || !data.event) return;
// 'calendly.event_scheduled' fires when a booking is completed if (data.event === 'calendly.event_scheduled') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'calendly_booking', form_builder: 'calendly', event_type_name: data.payload && data.payload.event_type ? data.payload.event_type.name : 'unknown', invitee_email: undefined // Do not push PII — track the event, not the person }); }
// 'calendly.date_and_time_selected' fires when a time slot is selected if (data.event === 'calendly.date_and_time_selected') { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'calendly_time_selected', form_builder: 'calendly' }); }});</script>Common mistakes across all form builders
Section titled “Common mistakes across all form builders”Firing on form load, not form success
Section titled “Firing on form load, not form success”The most critical rule: fire your conversion event only after the server confirms the submission was successful. Most of the patterns above wait for the success callback, success message display, or confirmation event. Don’t push form_submission on the initial click or form render.
Not handling form builders loaded asynchronously
Section titled “Not handling form builders loaded asynchronously”If the form builder’s script loads after your Custom HTML tag has already executed (because you fired on DOM Ready but the form builder loads on Window Loaded), your event listener setup code runs before the form builder is available. Move your Custom HTML tag to Window Loaded if you’re experiencing setup timing issues.
Storing PII from form fields
Section titled “Storing PII from form fields”Resist the temptation to capture what the user typed — especially email addresses. The dataLayer is accessible to anyone who opens the browser console. Push form metadata (form ID, step, success) but not field values.