Marketo Form Embeds: The Cheat Code to scaling your instance

Recently Jeff Kew posted on Linkedin about Marketo program structure and naming conventions:

And we got to talking about how best to scale Marketo forms. I said:

“Absolutely, and what can’t be tokenized in a program, I configure on the page itself via the embed script and forms API. Dynamic data values for attribution, custom destination URL/confirmation message, cookie ingestion, conversion events, etc.

The only place I configure anything for any form is either on the page itself (with a script template and easily updatable values and defaults built-in) or in tokens. I never ever set up webpage specific rules for a form.”

Ok, I must admit that this is only ~92.3% true. If not for the lack of token functionality in Smart Campaign lists/triggers, it would be 100% true. More on that below.

At any rate, I firmly believe that the Marketo form embed script is the “cheat code” to scaling your Marketo instance as far as possible. So that’s what this post is about!

How do most people setup forms and subsequent lead flow?

When it’s time to implement a Marketo form on a company website, the MOPs specialist typically hands the Marketo form embed script to the web dev, and they paste it into a page somewhere. Then, there is typically some sort of corresponding lead flow rule in Marketo: a smart campaign or a program that will look for form submissions on that specific URL, and if triggered they will:

  • Attribute – Typically some combination of updating the Person Source field, Acquisition Program field(s), and/or adding user to SF Campaigns.
  • Set as MQL status- Typically something like person status, maybe a timestamp for the qualification datetime, etc
  • Trigger some operational functions- Typically data enrichment and other web hooks, send appropriate internal notices, etc
  • Route the record to someone in the CRM- Typically sales. Sometimes Chili piper or similar product takes over here.

Here’s what this setup looks like visualized:

This works ok for new records (we’ll set aside lead lifecycle management for a moment, more on that below). Now, this organization has forms embedded and they are shipping new leads over to sales with some degree of consistency. Awesome!

Soon, the organization will have many of these rules set up for all the unique form embeds on their website. Again, this setup looks similar but there are now multiple static rules, tied to specific pages and/or specific form ID’s:

Now it’s time to make global changes to our lead flow rules. Yikes, we’re in trouble. Look how much is duplicated across our system and has to be manually edited? Very painful.

A common pain

This scenario is not uncommon. Example: Very experienced Marketo user advises a newer Marketo user to setup a unique trigger for every single page they have a form embedded.. and this experienced user goes on to say they have 7,000(!!!) pages on their website. I can’t imagine trying to manage that many rules; change management at scale becomes a nightmare.

I once inherited a Marketo instance with 100+ unique rules tied to form embeds on different pages of a website. I was tasked with building a working attribution model and optimizing our lead delivery methods for the entire system. Imagine the pain to update all of these inconsistently named, non tokenized, static rules that I had to click through to update manually. On top of that, there were about 40+ unique form ID’s being used across the site. it was awful to clean up.

Obviously, there is one quick improvement here: Use global forms!

I find it surprising how many people are still creating unique forms in Marketo for different programs: webinars, gated content, landing pages, or other perceived “edge cases”. Reducing the amount of form ID’s in your Marketo instance is among first steps to creating a more scalable instance.

It’s hard to reduce the number of unique form ID’s if you’re only using the Marketo form builder GUI to configure that form experience properly for each time it’s used. The form builder GUI doesn’t account for page specific customizations.

Taking control of your Forms

Having some basic web development skills, and knowing that all of the aforementioned Marketo lead flow rules were unique to specific web pages… I moved all of these configurations to those web pages themselves.

I control all of this from within the form embed script, using the Marketo Forms API and some very basic Javascript:

  • Custom submit button language
    • “Register now” or maybe “Get started” on the button, controlled at the form embed level rather than requiring a new form to update this text in the Marketo form builder GUI.
  • Custom confirmation messages
    • Display unique messages like “Thanks, we’ll be in touch!” vs “Thanks for registering for the webinar…”
  • Custom destination URLs
    • Let’s say you want to take a user to a new page after they submit the form. But this destination URL varies depending on what page the form is on? You can set that in the form embed script.
  • Lead Source and Lead Source Detail
    • Lead Source/detail fields are a key part of our attribution system. LS/LSD is set at the form embed level, captured upon submission, and First/Last is handled via a single dynamic program in Marketo.
  • Custom routing requests:
    • To route to a specific sales rep, overriding basic default rules.
  • Triggering a specific gated content download experience:
    • No need for a special form specific to gated content. You can use the same embed script on many pages and still trigger those unique gated content flows.
  • Custom scoring:
    • If you’d like to add additional points for this form, do that here**. (Requires flowboost to do the math to add this custom score the default score, as Marketo can’t add the values of 2 integer fields together)
  • SF Campaign member addition:
    • Set the SF campaign ID on the page itself. A single Smart campaign in SF dynamically handles every campaign ID submission, or even an infinite amount of SF campaigns if you’re using Flowboost.
  • Custom Conversion events can be called onSuccess of form submission:
    • VWO/Optimizely
    • Google analytics with dynamic values for Category/Event/label/Value/Etc
    • Advertising platforms
  • Ingesting query parameters or cookie data into hidden fields:
    • Example: Getting a query parameter out of the page URL for adding a member to a specific SF campaign. If you add something like ?sfcid=7015f000000pYCTAA2 to any campaign URL’s you can use a little javascript to pull down that ID from the URL and put it into a hidden field.
    • Example: Write a 1st party cookie that tracks your UTM touches into an array, load that array into hidden field to be captured for analysis

Here is what my form looks like in Marketo. Notice the hidden fields:

Here is what my embed script looks like.

 <script src="//yourmunchkinID.mktoweb.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_1001"></form>
<script>MktoForms2.loadForm("yourmunchkinID.mktoweb.com", "yourmunchkinID", 1001, function(form) {
//-----------Start configurable area. Don't edit anything above this line---------------
//----------------------------------------------------------------------------------
        //If this is gated content, please put the title of the gated piece.
        var gatedContentName = '';

        //Custom Button text - What do you want the Submit button to say?
        var customButtonText = '';
 
        //Custom confirmation message. If you'd like to display a custom message after submitting the form, set that message here
        var customConfirmationMessage = '';

        //Custom destination URL. Should the user be directed to an asset or a confirmation page after submitting? Set that here. This will override the Custom Confirmation Message. If neither of those variables are defined, the default confirmation message will be displayed to the user. 
        var customDestinationURL = '';

        //Custom lead Source and Lead Source Detail;configure those values here
        var customLeadSource = '';
        var customLeadSourceDetail = '';

        //To override standard routing rules, put the full name of the sales user the leads should be routed to.
        var customLeadRoutingRecipientName = '';

         //Custom scoring - Would you like to add additional points for this form fill? Add that integer here. 
        var customScoring = '';

        //Is there a Salesforce campaign ID you'd like to use for this form? This will override any SF CampaignID included in the designated URL query parameter for SFID. 
        var SFCampaignID = '';


//-----------End configurable area. Don't edit anything below this line-------------------------
//------------------------------------------------------------------------------------------

//Dynamic page info to capture: 
    var pageURL = window.location;
    var pageTitle = document.title;

//Default values
    var defaultButtonText = 'Submit';
    var defaultConfirmationMessage = 'Thank you! A representative will be following up with you shortly.';
    var defaultLeadSource = 'domain.io';
    var defaultLeadSourceDetail = 'Contact Us Form - ' + pageTitle + " - " + pageURL;
    var defaultGatedContentConfirmationMessage = ''

//Check to see if Variables were set above, and if not, prepare to use the defaults

    //Custom button text: If it's empty use the default, otherwise use the custom
    if (!customButtonText) {
        var buttonText = defaultButtontext;
        } else {
            var buttonText = customButtonText;
            };

    //Gated Content Default Confirmation messages: If we've filled in the gatedcontent variable, we can presume this form is intended for gated content download, and should use a specfic default confirmation message. 
    if (gatedContentName) {
        var defaultConfirmationMessage = 'Success! Check your inbox for your content download. Thank you!';
        };

    //Custom Confirmation message: If this variable is empty, then use the default confirmation message. 
    if (!customConfirmationMessage) {
        var confirmationmessage = defaultConfirmationMessage;
        } else {
            var confirmationmessage = customConfirmationMessage;
                };
    //Custom Lead Source: if it's empty, use the default otherwise use the custom lead source 
    if (!customLeadSource) {
            var leadSource = defaultLeadSource;
        } else {
            var leadSource = customLeadSource;
        };

    //Custom lead Source Detail: if it's empty, use the default otherwise use the custom LSD
    if (!customLeadSourceDetail) {
            var leadSourceDetail = defaultLeadSourceDetail;
        } else {
            var leadSourceDetail = customLeadSourceDetail;
        };

    
    //Get SF Campaign ID Query param in URL.
    const params = new Proxy(new URLSearchParams(window.location.search), {
    get: (searchParams, prop) => searchParams.get(prop),
        });
    let sfcidValue = params.sfcid;

    //Check to see if SF Campaign ID was set here, or if it was included in the URL of the page. Load into variable. 
     if (!SFCampaignID) {
            var SFCampaignID = sfcidValue;
        };

//Take all of our variables, and feed them into the form
    //Button text editor
    form.getFormElem().find('button').text(buttonText);

    // Set values for the hidden fields
    form.vals({
 			"gatedContentDownload": gatedContentName,
            "leadSourceFormCapture": leadSource,
            "leadSourceDetailCapture": leadSourceDetail,
            "customRouting": customLeadRoutingRecipientName,
            "sFCampaignIDCapture": SFCampaignID,
            "customScoreCapture": customScoring
            });

    //onSuccess handler
    form.onSuccess(function(values, followUpUrl) {
         //If CustomdestinationURL is not empty, then take the user there
            if (customDestinationURL) {
                window.parent.location = customDestinationURL;
                return false;
            } else { //If it is empty, then fallback on confirmation messages. 
                
                form.getFormElem().hide();
                document.getElementById("successAndErrorMessages").innerHTML = confirmationmessage;
                return false;
            };
            //End 
            ;
    });
    }); 
</script>
<p id="successAndErrorMessages"></p> 

Disclaimer #1: This script is not optimized for performance. It is heavily commented and the JS is written in a way that emphasizes readability and detail for this blog post; it could be a significantly shorter script if that was our goal.

Disclaimer #2: I’m not a “real” web dev, and that’s sort of the point here. You don’t have to be an accomplished developer or engineer to learn how to use Marketo’s forms API. MOPs specialists can 5-10X their impact and career trajectory by learning a bit of code, starting with basic HTML/CSS, and progressing into Javascript, Apex, Velocity, SQL/MySQL, Jekyll/Liquid, Jinja, even PHP, python, whatever. You don’t have to remember everything you encounter, or even remember the syntax for these languages. It’s all easily found on Google when you need it. Get comfortable reading and dissecting code to understand what it’s doing, and you will likely be able to piece together whatever you need to automate in different systems.

I recently recorded a podcast on this topic with Crissy Saunders, CEO at CS2 Marketing which can be found here!

Ok, back to it. Once all of these unique form lead flows and data values have been moved out of Marketo, to be configured in the embed script, you can build one single lead flow program in Marketo that is modular, dynamic, and flexible enough to handle virtually any state that a lead record might currently be in.This is our lead lifecycle management program.

A scalable Lead Lifecycle program

To properly account for the lead lifecycle means that we have to check several things before we know what exactly to do with any MQL record. Things like:

  • Do they exist already?
  • Are they currently an MQL or being worked by Sales?
  • Are they owned by sales rep that’s no longer at the company?
  • Are they a current customer?
  • Were they an MQL already once before?
  • Are they a lead, or a contact?

These are all questions that impact how you process, route, count, and follow up with qualified leads.

So, in the scenario we referenced earlier; where someone has created a unique rule for every form embed…where do you put all of these lead lifecycle checks? In each one of these rules? With 6 yes/no questions above, there are 64 unique possible combination scenarios… accounting for them all within a single form submission flow is just not realistic.

The most scalable solution to this is to build one dynamic program that handles all MQLs from all forms, pages, etc. It checks each of the above criteria separately, regardless of what form was submitted or how it was identified as an MQL. We end up with a single centralized location where we run all leads through, to determine what should happen when they become ‘qualified’. Here’s what that looks like visually:

Here’s how it works:

When a lead is captured (in our case, via form) it receives it’s LS/LSD attribution, and the engagement activity is scored per the standard scoring engine.

Then, we use a scoring threshold to determine if a lead is an MQL. Our threshold is currently 50 points. Our handraiser forms are exactly 50 points for example.

We also have score reset rules to allow users to go back to the top of the flow; closed lost, set status to “Return to Marketing’, or no contact after 30 days will automatically reset this lead’s score back to 0 and allows them to become “Requalified” again in the future. A lead record can go through this lifecycle flow as many times as it takes for them to become a customer.

Building smarter flows with Smart Campaigns

To build this flow into Marketo, I take a somewhat unique approach. I build out my visual flow chart first (shown above), putting the criteria checks in the flow step of the smart campaign, rather than the Smart List/Trigger.

Wherever there is a decision step, I build a single smart campaign that checks one single yes/no conditional check, and then request the next appropriate smart campaign in the visual flow. This way, we know we checked every condition but didn’t accidentally filter anyone out along the way by using overly robust filters.

Here’s what the “Is Owner Active” decision steps looks like in Marketo, for example:


This approach scales pretty well, and it’s infinitely easier to debug where a lead flow went wrong. The activity log is much clearer this way as well. If we decide that we need to add another question somewhere in the flow; no problem. We can do that by creating the new rule, and insert it into the flow by editing the conditional check before and after the spot we want to enter this new rule.

Quick tip for building: construct each decision step rule in reverse order of your visual flow. Start at the end, build the last smart campaign, and once you’re confident the simple rule looks correct, activate it, and then move up the flow chart, to build the next decision step in reverse order. It just saves a lot of back and forth clicking in the Marketo UI compared to starting at the top and going down.

Custom Routing Explainer

For our custom routing conditional check step, we could get 100% dynamic with our custom lead recipient if Marketo allowed SF User ID’s in the ‘Change owner’ or ‘Sync to SFDC’ flow step. Then we could use a lead token to dynamically tell Marketo which SF user to assign it to. Unfortunately, tokens are not supported in the “Change owner” flow step. Alas, I resort to a “case” style configuration for detecting sales owner name and assigning to static value for that SF user. It looks like this:

Go vote for Gregoire Michel’s idea here to make tokens available in Sync to SF Flow step: Vote Here

To make my original Linkedin claim 100% accurate, it would be very valuable if Marketo allowed us to put program level tokens into Smart Campaign triggers. We could enter things like URL’s, hidden field values as tokens in templated programs to trigger gated content experiences, etc. Go vote for this idea here: Vote here

Lead Source and lead Source detail handler explainer:

I like to define Lead Source and LSD as the action that generated the lead record; more specifically where/how did we actually capture this information? I also like to store a “most recent” version of these fields to see what action a lead has taken most recently to submit their info to us.

Then, in Marketo I create a smart campaign that detects these values being captured by the form in our special ‘capture’ fields, and then will load them into the native LS/LSD fields the first time, and will overwrite the “most recent” every time. This setup provides a layer of flexibility to log and preserve a “first” and “most recent” LS/LSD.

Here’s what that looks like in Marketo for Lead Source Capture:

Dynamic SF Campaign membership handler:

As illustrated above, most Marketo users will create unique, static rules tied to a specific web page or form ID to take action when a lead submits that form on that page. As part of their attribution efforts, they will often use this rule to add a user to a specific Salesforce campaign. This is fine on a small scale, but it quickly grows out of control as you continue to create more and more campaigns and corresponding rules.

The best way I’ve found to handle this is to use a dynamic SF Campaign membership handler.

I can either set the SF campaign in the embed script via the variable, or in a URL query parameter. Then, we use our form embed script to ensure that it’s ingested into a hidden field on my form, named SFCampaignIDCapture.

In Marketo, we have a single rule that can handle these dynamically; it uses a token to add any lead to any SF campaign that we define in the embed script or query parameter. This is how that smart campaign looks in Marketo:

You may have noticed in the smart list here there are 2 triggers. The first one is for when a user syncs to SF for the first time (SF Type becomes filled)  and the 2nd trigger is when the SFCampaignID data value is updated. We combine filters to ensure that this rule will only run once a user is in SF, and they have some SFCampaignID field value. Now, if a user submits a form with a SFCampaignID value, but they aren’t in SF yet… we will wait until they run through our MQL flow, get synced and *then* we add them to the SF campaign, and wipe the value so they can run through this rule again if they submit another form with another SFCampaignID.

Bonus tip; you can handle multiple campaigns at once using Flowboost to create a multi touch view. In the past, I’ve worked with Sanford Whiteman to build a Flowboost solution that takes a cookie containing a JSON array of up to 20 UTM_Campaign values that contained Salesforce Campaign IDs, loops through them all, and makes an API call back to Marketo to add the user to all the Salesforce campaigns detected. This creates a multi-touch model for campaigns that a user had interacted with on previous website visits before finally submitting a Marketo form. Maybe I’ll explain this topic in more detail in another post.

Scale it to the extreme:

With this dynamic Form -> Lead flow setup, you end up with:

  • A small amount of programs in Marketo that can dynamically handle any form embeds you create in your website, without ever having to create more
  • A very limited number of global forms to maintain, but with all the flexibility you’d ever need for a form
  • The beginnings of a very simple but powerful multi-touch attribution model:
    • A dynamic SF campaign handler to track traffic driving campaigns (Channel)
    • A dynamic Lead Source/Lead Source Detail handler that also captures “most recent” copies of this data to track lead capture events (Source)

One could say… but Jordan, you still have 10, 20, 30, maybe even 7,000 pages each with unique form embed scripts on them. What happens if you need to make a global change to the actual scripts themselves, say to include a value for a new hidden field on your form?

This is where the “global” concept comes back into play. If your website is running on WordPress, you can easily centralize your form embed scripts into a single location using a shortcode. You can code your embed script into the functions.php file of your WordPress theme, and then just pass the configurable variables for each embed script/page by including them as parameters in the shortcode.

This means you have now have a global version of your custom embed script. Want to edit all your form embeds? Cool, edit the single script, and just be sure to maintain support for all the custom parameters you have entered on different pages.

The only thing that is ever manually edited when you want to create a new form embed is the parameters in the short code. It becomes very simple to deploy highly customized Marketo forms. Here’s what I would put into a wordpress page to embed a form:

Blank shortcode is the simplest, shortest way to deploy a form and it would use our defaults for formID, Lead Source/Detail, etc:

Shortcode with parameters can override defaults if filled in:

Shortcode with parameters filled in to override defaults with these values:

This makes deploying forms incredibly easy and scalable. We can handle thousands of unique form embed configurations without having to build any more Marketo smart campaigns or programs.

If people are interested in seeing more of this, I can create a detailed post on how to accomplish the shortcode setup. This can also be accomplished without WordPress, but it’s not as elegant and requires more custom web development.

Summary

So, that was a lot. While it’s not necessary to use all of this in your instance, I share it all because I think it’s worth illustrating how these ideas and components can work together to create a very scalable Marketo instance and website integration.

Advantages to this Form and Lifecycle management approach:

  • You can stop building a new rule in Marketo every time you want to embed a form somewhere on your website. In my experience, these active smart campaigns are a key contributor to performance issues in a marketo instance.
  • You can reduce the amount of page level embeds for things like a newsletter subscription form. Place it in the global footer of your website just one time, and it can dynamically grab the current page title or article title as part of your lead source detail capture.
  • You’ve centralized the lead lifecycle steps into a single program. One could argue that you could still do this part without the custom embed scripts, but they really work well together to make your entire instance more scalable.
  • You’ve solved for the problem of leads going cold, return to marketing, closed lost, etc. I see SO many Lead Lifecycle models from the tool that look cool in theory… but are riddled with bugs and loopholes as leads stray from the anticipated journey paths.
  • Javascript based form configurations provide far more flexibility than the Marketo GUI ever could, reducing the amount of unique forms required in your instance. Destination URL for example can only be set once for a global form. You’d be stuck with that destination URL, but you can change it with javascript every time you embed that form.
  • Furthermore, using a language like javascript (or Jquery, or other JS libraries for reading/writing cookies) opens a whole layer of dynamic-tokens-on-steroids type functionality. You can do simple things like dynamically pass the specific CTA button language that was used to trigger a modal form, or even more complex things like parsing or cleaning an array of UTM touches and timestamps to pass structured traffic attribution into hidden fields, etc. The sky (err, the browser) is really the limit with Javascript here.
  • With the LeadSource/Lead Source detail capture function, you get both first lead capture and most recent lead capture. This is one small step to a robust, dynamic multi touch attribution from within Marketo. Maybe I’ll make another post on that sometime.
  • With the dynamic MQL lead flow model, you can easily add new steps into the flow, without having to review/update every single lead flow rule you’ve built before. It makes each step in the flow modular and ready to evolve over time.
  • You might notice there are no confirmation landing pages, or thank you pages mentioned anywhere. I really don’t see the point of building them if you can control the submission experience and data flows from the form embed script itself. You don’t need to wait to load another entire web page to tell the user ‘Success!’ and we don’t use these forms in Marketo for any triggers.

Considerations:

  • It takes a very basic level of Javascript knowledge to update the script to your needs.
  • This specific version of the script here is not really optimized for website performance. While it will add an almost insignificant amount of weight to your page load, it’s not nothing. If you’re concerned about page performance I would recommend working with your web team to setup page caching, consider minification, and otherways to reduce the size of this script.
  • Sometimes website GUI’s don’t like the comments and spaces in the script, and might give you an error when trying to load a form. Shortcodes are very helpful in this scenario, but work with your web team to determine how/where you can enable the ability to put commented, multi-line HTML/JS code.
  • Anyone who knows how to view the source code of a web page would be able to see the content of this script. Don’t put any information in the script comments configurable variables that you don’t want in the wild.

Thanks for reading! I’m curious to hear what people think. If you’ve spotted errors/inaccuracies, or if I’ve not explained anything in enough detail I’d love to hear from you. Please comment here or find me on linkedin and shoot me a message!