Website / WordPress

Website

WordPress

Connect Contact Form 7, Gravity Forms, WPForms, Elementor and other WordPress forms.

Keep the forms you already have on WordPress. Your form plugin still saves every entry, sends its notification emails and runs its add-ons. Once the plugin confirms a submission, Cauliflower sends the lead to a router and opens the right rep's calendar on top of the plugin's confirmation message.

Setup has two parts: add the widget's loader to your site once, then add the short snippet for your form plugin. This page covers Contact Form 7, Gravity Forms, WPForms, Elementor forms, HubSpot forms and plain HTML forms.

Before you start

  • A router that is Live, with the Website form entry point turned on in its trigger. The starter router, inbound, already is.
  • The router's slug, shown on Routers → your router → Install, along with your Cauliflower URL and workspace slug.
  • An administrator account on your WordPress site. On WordPress.com, adding plugins and scripts needs a plan that supports plugins.

Allow your domains

In Cauliflower, open Settings → Workspace → Widget security & qualification → Allowed website origins and add every domain the form runs on, one per line, including a staging site if you test there:

Text
https://example.com
https://www.example.com
https://staging.example.com

If the list is empty, every website is allowed. See Widget security.

Add the loader

The loader is the same on every site. Replace https://cal.example.com with your Cauliflower URL and acme with your workspace slug, or copy it from Routers → your router → Install → 1. Add the loader, where both are filled in:

HTML
<script>
  (function (w, d) {
    w.cauliflower = w.cauliflower || function () { (w.cauliflower.q = w.cauliflower.q || []).push(arguments) };
    var s = d.createElement("script"); s.async = true; s.src = "https://cal.example.com/embed.js"; d.head.appendChild(s);
  })(window, document);
  cauliflower("init", { workspace: "acme" });
</script>

Add it to every page in one of these ways, and put your form plugin's snippet right below it in the same place.

WhereHow
WPCode plugin, formerly Insert Headers and FootersInstall WPCode, open Code Snippets → Header & Footer, paste into Footer and save. The free version is enough.
Your themeIn a child theme's functions.php, print it from the wp_footer hook, as shown below. Edits to a parent theme are lost when it updates.
Google Tag ManagerAdd a Custom HTML tag with the code and the All Pages trigger, then publish the container.
php
add_action( 'wp_footer', function () {
	?>
	<script>
		/* The loader, then your form plugin's snippet */
	</script>
	<?php
} );

Connect your form plugin

Each snippet waits until the plugin has accepted the submission, then reads the values and calls route. Leads the plugin rejects, for a missing field or a failed spam check, are never routed. route adds the page URL, the referrer and UTM parameters, and shows visitors the same outcome as any widget form: the calendar for qualified leads, the path's message for disqualified or assigned leads, or a redirect.

In every snippet, ROUTERS says which forms are routed and to which router, so newsletter signups and other forms are left alone, and MAP copies your field names to Cauliflower's. Keys in MAP are Cauliflower's names, values are the names in your form's HTML. Cauliflower already recognizes common names such as email, first_name or company; see Field mapping.

Show a message, not a redirect

Set each routed form to show a confirmation message on the same page. A redirect, or a confirmation that loads another page, takes the visitor away before they can book. For leads who should go to another page, add a Redirect step to the router path instead. See Path steps.

Contact Form 7

Contact Form 7 announces a successful submission with a wpcf7mailsent event, which includes the submitted values.

HTML
<script>
  // Contact Form 7: routes the lead once the form has been sent.
  (function () {
    // Contact form ID → router slug.
    var ROUTERS = { 123: "inbound" };
    // Cauliflower's field names → yours. "your-email" is the email field in Contact Form 7's default form.
    var MAP = { email: "your-email" };
    document.addEventListener("wpcf7mailsent", function (event) {
      var router = ROUTERS[event.detail.contactFormId];
      if (!router) return;
      var lead = {};
      event.detail.inputs.forEach(function (input) {
        lead[input.name] = input.name in lead ? [].concat(lead[input.name], input.value) : input.value;
      });
      for (var key in MAP) if (MAP[key] in lead) lead[key] = lead[MAP[key]];
      cauliflower("route", { router: router, lead: lead });
    });
  })();
</script>
  • Form ID: the number after post= in the address bar when you edit the form under Contact → Contact Forms. Newer versions of Contact Form 7 put a short hash in the shortcode instead, so don't copy it from there.
  • Field names: the name in each form tag, for example your-email in [email* your-email]. The default form's your-name is recognized as the full name.
  • Mail must send: the event only fires once Contact Form 7 has sent the form's email. If your site can't send email, the form shows an error and the lead isn't routed, so fix email delivery first, for example with an SMTP plugin.
  • Remove any add-on that redirects after submission.

Gravity Forms

Gravity Forms fires gform_confirmation_loaded when an AJAX form shows its confirmation. The form must be embedded with AJAX on: turn on AJAX in the Gravity Forms block's settings, or add ajax="true" to the shortcode. Set the form's confirmation type to Text.

HTML
<script>
  // Gravity Forms: routes the lead once an AJAX form shows its confirmation.
  (function () {
    // Form ID → router slug.
    var ROUTERS = { 1: "inbound" };
    // Cauliflower's field names → Gravity Forms input names, which follow the field IDs.
    var MAP = { email: "input_3", first_name: "input_1.3", last_name: "input_1.6", company: "input_4" };
    // The confirmation replaces the form, so remember its values while the visitor fills it in.
    var values = {};
    function remember(event) {
      var form = event.target.closest && event.target.closest("form");
      var match = form && /^gform_(\d+)$/.exec(form.getAttribute("id") || "");
      if (match) values[match[1]] = new FormData(form);
    }
    ["input", "change", "submit"].forEach(function (type) { document.addEventListener(type, remember, true); });
    function onJQuery(fn) { window.jQuery ? fn(window.jQuery) : window.addEventListener("load", function () { fn(window.jQuery); }); }
    onJQuery(function ($) {
      $(document).on("gform_confirmation_loaded", function (event, formId) {
        var router = ROUTERS[formId], data = values[formId];
        if (!router || !data) return;
        var lead = {};
        for (var key in MAP) if (data.get(MAP[key])) lead[key] = data.get(MAP[key]);
        cauliflower("route", { router: router, lead: lead });
      });
    });
  })();
</script>
  • Form ID: shown in the forms list under Forms, and in the shortcode.
  • Input names come from field IDs: input_3 is field 3. A Name field has one input per part, input_1.3 for the first name and input_1.6 for the last name when it's field 1. The form editor shows each field's ID in its settings. Gravity Forms' names don't describe the fields, so only fields listed in MAP are sent.
  • To route on more fields, add them to MAP, for example company_size: "input_5".

WPForms

WPForms fires wpformsAjaxSubmitSuccess when an AJAX submission succeeds. AJAX is off by default: in the form builder, open Settings → General, scroll to Advanced, turn on Enable AJAX form submission and save. Under Settings → Confirmations, set the confirmation type to Message.

HTML
<script>
  // WPForms: routes the lead once an AJAX submission succeeds.
  (function () {
    // Form ID → router slug.
    var ROUTERS = { 123: "inbound" };
    // Cauliflower's field names → WPForms input names, which follow the field IDs.
    var MAP = { email: "wpforms[fields][1]", first_name: "wpforms[fields][0][first]", last_name: "wpforms[fields][0][last]", company: "wpforms[fields][2]" };
    // The confirmation replaces the form, so remember its values while the visitor fills it in.
    var values = {};
    function remember(event) {
      var form = event.target.closest && event.target.closest("form");
      var match = form && /^wpforms-form-(\d+)$/.exec(form.getAttribute("id") || "");
      if (match) values[match[1]] = new FormData(form);
    }
    ["input", "change", "submit"].forEach(function (type) { document.addEventListener(type, remember, true); });
    function onJQuery(fn) { window.jQuery ? fn(window.jQuery) : window.addEventListener("load", function () { fn(window.jQuery); }); }
    onJQuery(function ($) {
      // Listen on each form, as WPForms' own examples do.
      $(function () {
        $("form.wpforms-form").on("wpformsAjaxSubmitSuccess", function () {
          var formId = (this.getAttribute("id") || "").replace("wpforms-form-", "");
          var router = ROUTERS[formId], data = values[formId];
          if (!router || !data) return;
          var lead = {};
          for (var key in MAP) if (data.get(MAP[key])) lead[key] = data.get(MAP[key]);
          cauliflower("route", { router: router, lead: lead });
        });
      });
    });
  })();
</script>
  • Form ID: shown in the forms list under WPForms → All Forms, and in the shortcode.
  • Input names follow the field IDs shown in the form builder: wpforms[fields][1] is field 1. A Name field in the First Last format sends [first] and [last] parts. Only fields listed in MAP are sent.

Elementor forms

The Form widget in Elementor Pro fires submit_success once a submission has gone through. Elementor names each field after the ID in its Advanced tab, so a form whose fields have the IDs name, email and company needs no mapping at all.

HTML
<script>
  // Elementor forms: routes the lead once a submission succeeds.
  (function () {
    // Form Name (Content → Form Fields) → router slug.
    var ROUTERS = { "Demo request": "inbound" };
    // Cauliflower's field names → your Elementor field IDs, when they differ.
    var MAP = {};
    // Elementor resets the form after a successful submission, so remember its values while the visitor fills it in.
    var values = new WeakMap();
    function remember(event) {
      var form = event.target.closest && event.target.closest("form.elementor-form");
      if (form) values.set(form, new FormData(form));
    }
    ["input", "change", "submit"].forEach(function (type) { document.addEventListener(type, remember, true); });
    function onJQuery(fn) { window.jQuery ? fn(window.jQuery) : window.addEventListener("load", function () { fn(window.jQuery); }); }
    onJQuery(function ($) {
      $(document).on("submit_success", function (event) {
        var form = event.target, router = ROUTERS[form.getAttribute("name")], data = values.get(form);
        if (!router || !data) return;
        var lead = {};
        data.forEach(function (value, name) {
          var id = /^form_fields\[([^\]]+)\]/.exec(name);
          if (!id || typeof value !== "string") return;
          lead[id[1]] = id[1] in lead ? [].concat(lead[id[1]], value) : value;
        });
        for (var key in MAP) if (MAP[key] in lead) lead[key] = lead[MAP[key]];
        cauliflower("route", { router: router, lead: lead });
      });
    });
  })();
</script>
  • Form Name is the first setting of the Form widget, under Content → Form Fields. Use a name that's unique on your site.
  • Under Actions After Submit, remove the Redirect action from routed forms.

HubSpot forms

HubSpot forms embedded on WordPress, with HubSpot's plugin or an embed code, use the same snippet as on HubSpot's own pages. See HubSpot CMS and add its snippet below the loader instead of in HubSpot's settings.

Plain HTML forms

A form you wrote yourself, for example in a Custom HTML block, can use the widget's standard binding. Add the router's slug to the form, and Cauliflower handles the submission:

HTML
<form data-cauliflower-router="inbound">
  <input name="email" type="email" placeholder="Work email" required />
  <input name="company" placeholder="Company" />
  <button type="submit">Book a demo</button>
</form>

Don't use this binding on forms that belong to a plugin. It takes over the submission, so the plugin never receives the entry. To keep sending the form to its action URL as well, see Keeping your existing submission.

Other form plugins

The same approach works for any plugin that confirms submissions with a JavaScript event. Most do, such as Ninja Forms, Fluent Forms, Formidable Forms and Forminator. Listen for the plugin's success event, collect the values, and call cauliflower("route", { router: "inbound", lead: values }). See Custom forms.

Booking buttons

The loader also lets any button or link open a booking link in a modal, such as a "Book a demo" button in your menu. Give the element a data-cauliflower-link attribute with the booking link's slug, in a Custom HTML block or a page builder's attribute settings:

HTML
<a href="#" data-cauliflower-link="product-demo">Book a demo</a>

Embed the whole router instead

To skip the form plugin, embed the router's hosted page: a form built from the router's fields, your team's photos and the calendar, all in one. Copy the code under Routers → your router → Install → Embed the hosted page into a Custom HTML block. It doesn't need the loader. See Hosted router page.

  • Performance plugins that delay, defer or combine JavaScript, such as WP Rocket, LiteSpeed Cache, Autoptimize or Perfmatters, can stop the loader and snippets from running in time. Exclude cauliflower and embed.js from those options. In WP Rocket, add both to File Optimization → Delay JavaScript execution → Excluded JavaScript Files.
  • Page caching needs no changes. Everything runs in the visitor's browser.
  • Cookie consent plugins that block scripts until a visitor accepts keep the calendar from opening for visitors who haven't. The widget sets no cookies; it keeps the visit's UTM parameters in the browser's session storage, so the lead is attributed to the campaign they came from. Decide with whoever owns your privacy policy how to categorize it.
  • Security headers: if your site sends a Content Security Policy, allow your Cauliflower URL in script-src, connect-src and frame-src.

Go-live checklist

  • The router is Live and its Website form entry point is on.
  • The loader and your plugin's snippet are on every page with a routed form, with your Cauliflower URL and workspace slug.
  • ROUTERS lists each form that should route, with the right router slug, and no newsletter or other forms.
  • The email field is recognized, by its name or through MAP.
  • Those forms show a confirmation message on the same page, with AJAX on for Gravity Forms and WPForms.
  • Allowed website origins lists every production domain, and the staging domain if you test there.
  • A test submission, with a teammate's email address, opens the calendar and shows up in the plugin's entries and in the router's Logs.
  • A test with a personal email address, or anything else your paths disqualify, shows the message you expect.

Troubleshooting

Open the browser's developer console on the live page to see errors from Cauliflower. The most common problems:

What you seeCause and fix
The plugin shows its confirmation, but no calendar opens and nothing is in the router's LogsThe code isn't loading or the form isn't in ROUTERS. View the page source to check the loader is there, check the form ID, and exclude the code from your performance plugin's JavaScript options.
The page reloads when the form is submittedAJAX is off for the form. Turn it on, as described for Gravity Forms and WPForms.
The calendar opens, then the page changesThe form redirects after submission. Switch it to a confirmation message.
The plugin has no entry for the submissionThe form uses data-cauliflower-router or cauliflower("form", …). Remove that binding and use the plugin's snippet.
"A valid email is required to route a lead"Cauliflower didn't recognize the email field. Add it to MAP.
"This website is not allowed to use this workspace's widget"The domain isn't in Allowed website origins. Add it, including www. and any staging domain.
"This router is paused" or "This router doesn't accept website form submissions"Set the router to Live and turn on its Website form entry point.
"Too many submissions. Please wait a minute."Public routing allows 30 submissions per minute from one IP address. Wait a minute between test bursts.
The calendar opens, but the lead goes down the wrong pathOpen the lead from the router's Logs tab. Its page shows the Submitted data that arrived and how each path's conditions were evaluated.

To check a field's name, right-click the field on the live page, choose Inspect and look at its name attribute.