Intelligently allowing or blocking field updates from form fills (using FlowBoost)

Should form fills update existing values? It depends.

Existing values may be more trusted, especially if they’re vetted by sales and/or already used for lead routing. On the flipside, existing values can have low trust or be mere placeholders, and you definitely want the lead to supply a better value.

Relying solely on Admin » Field Management » Block Field Updates therefore doesn’t work. With that setting, either you let any form update field X, forever — or you never let any form do anything to field X unless it’s creating a brand new lead.

Luckily, Marketo stores the values from the form in the Filled Out Form activity details, even if the field is blocked from updates. So using FlowBoost to make a couple of REST API calls, you can grab those raw values and then intelligently merge them with existing values, applying any JS logic you can imagine.

Block field updates to start

First, you do want to use Block Field Updates (we’re supplementing it, not replacing it completely):

Customize which updates you allow/deny

The full FlowBoost payload below is unexpectedly long because we need to decode the API response using a PHP unserializer — more on that another day, maybe! — but the only part you need to worry about is the finalizeValues() function.

In the following example, if a First Name was submitted on the form and the lead’s current value is the special string [Unknown], we let the submitted First Name take effect by adding it to the final object:

function finalizeValues(currentValues, formValues){
  const final = {};

  if(formValues.FirstName && currentValues.FirstName == "[Unknown]") {
    final.FirstName = formValues.FirstName;
  }
  
  return final;
}

Now let’s add another rule. If a non-empty Number of Employees is submitted and the person’s current Lead Status is Open, then let them update Number of Employees:

function finalizeValues(currentValues, formValues){
  const final = {};

  if(formValues.FirstName && currentValues.FirstName == "[Unknown]") {
    final.FirstName = formValues.FirstName;
  }

  if(formValues.NumberOfEmployees && currentValues.Status == "Open") {
    final.NumberOfEmployees = formValues.NumberOfEmployees;
  }
  
  return final;
}

Add any properties you might set in final under Response Mappings:

(If you don’t include a given property in final, Marketo ignores the mapping completely.)

Imagine the power that’s now at your fingertips!

Get the code

Make sure you have ?authoringenv=pro in the FlowBoost URL and start with this payload:

// add whatever fields you want to consult in finalizeValues()
const currentValues = {
  id: {{Lead.Id}},
  FirstName: {{Lead.First Name}},
  Status: {{lead.Lead Status}}
}

// 2 minutes is probably OK but 5 to be safe
const lookbackMinutes = 5;

// replace with your LaunchPoint API values
// the role only needs Read access to Activity
const MarketoConfig = {
  munchkinId: "123-ABC-456",
  clientId: "8684cae0-029c-41be-bada-35e50210d824",
  clientSecret: "0ee933056c5f42e781c36edce500a775"
};

// implement your fancy custom merge logic
function finalizeValues(currentValues, formValues){
  const final = {};

  if(formValues.FirstName && currentValues.FirstName == "[Unknown]") {
    final.FirstName = formValues.FirstName;
  }

  if(formValues.NumberOfEmployees && currentValues.Status == "Open") {
    final.NumberOfEmployees = formValues.NumberOfEmployees;
  }
  
  return final;
}

/* --- NO NEED TO TOUCH BELOW THIS LINE --- */

const ACTIVITY_TYPE_FILLED_OUT_FORM = 2;

const startTime = new Date();
startTime.setMinutes(startTime.getMinutes() - lookbackMinutes);

FBHttp.fetch(
  `https://${MarketoConfig.munchkinId}.mktorest.com/identity/oauth/token?grant_type=client_credentials&client_id=${MarketoConfig.clientId}&client_secret=${MarketoConfig.clientSecret}`
)
.then( resp => resp.json() )
.then( body => ({ Authorization: `Bearer ${body.access_token}` }) )
.then( authHeader => 
  FBHttp.fetch(
    `https://${MarketoConfig.munchkinId}.mktorest.com/rest/v1/activities/pagingtoken.json?sinceDatetime=${startTime.toISOString()}`,
    { headers: authHeader }
  )
  .then( resp => resp.json() )
  .then( body => body.nextPageToken )
  .then( nextPageToken => 
    FBHttp.fetch(
      `https://${MarketoConfig.munchkinId}.mktorest.com/rest/v1/activities.json?activityTypeIds=${ACTIVITY_TYPE_FILLED_OUT_FORM}&leadIds=${currentValues.id}&nextPageToken=${encodeURIComponent(nextPageToken)}`,
      { headers: authHeader }
    )
    .then( resp => resp.json() )
    .then( body => body.result?.[0].attributes.find(attr => attr.name == "Form Fields").value )
    .then( serializedFormData => serializedFormData ? phpUnserialize(serializedFormData) : null )
    .then( formData => {
      if(!formData) throw "No recent form fill, check config."

      return {
        posted: formData,
        final: finalizeValues(currentValues,formData)
      };
    })
  )
)
.then(success)
.catch(failure);


/**
 * BD808's [php-unserialize-js](https://github.com/bd808/php-unserialize-js)
 * @author Bryan Davis
 * @version 1.3.0
 * @copyright © 2013, Bryan Davis and contributors
 * @license MIT
 */
(function(root, factory) {
  /*global define, exports, module */
  "use strict";

  /* istanbul ignore next: no coverage reporting for UMD wrapper */
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define([], factory);
  } else if (typeof exports === 'object') {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory();
  } else {
    // Browser globals (root is window)
    root.phpUnserialize = factory();
  }
}(this, function() {
  "use strict";

  /**
   * Parse php serialized data into js objects.
   *
   * @param {String} phpstr Php serialized string to parse
   * @return {mixed} Parsed result
   */
  return function(phpstr) {
    var idx = 0,
      refStack = [],
      ridx = 0,
      parseNext // forward declaraton for "use strict"

      , readLength = function() {
        var del = phpstr.indexOf(':', idx),
          val = phpstr.substring(idx, del);
        idx = del + 2;
        return parseInt(val, 10);
      } //end readLength

      ,
      readInt = function() {
        var del = phpstr.indexOf(';', idx),
          val = phpstr.substring(idx, del);
        idx = del + 1;
        return parseInt(val, 10);
      } //end readInt

      ,
      parseAsInt = function() {
        var val = readInt();
        refStack[ridx++] = val;
        return val;
      } //end parseAsInt

      ,
      parseAsFloat = function() {
        var del = phpstr.indexOf(';', idx),
          val = phpstr.substring(idx, del);
        idx = del + 1;
        val = parseFloat(val);
        refStack[ridx++] = val;
        return val;
      } //end parseAsFloat

      ,
      parseAsBoolean = function() {
        var del = phpstr.indexOf(';', idx),
          val = phpstr.substring(idx, del);
        idx = del + 1;
        val = ("1" === val) ? true : false;
        refStack[ridx++] = val;
        return val;
      } //end parseAsBoolean

      ,
      readString = function(expect) {
        expect = typeof expect !== "undefined" ? expect : '"';
        var len = readLength(),
          utfLen = 0,
          bytes = 0,
          ch, val;
        while (bytes < len) {
          ch = phpstr.charCodeAt(idx + utfLen++);
          if (ch <= 0x007F) {
            bytes++;
          } else if (ch > 0x07FF) {
            bytes += 3;
          } else {
            bytes += 2;
          }
        }
        // catch non-compliant utf8 encodings
        if (phpstr.charAt(idx + utfLen) !== expect) {
          utfLen += phpstr.indexOf('"', idx + utfLen) - idx - utfLen;
        }
        val = phpstr.substring(idx, idx + utfLen);
        idx += utfLen + 2;
        return val;
      } //end readString

      ,
      parseAsString = function() {
        var val = readString();
        refStack[ridx++] = val;
        return val;
      } //end parseAsString

      ,
      readType = function() {
        var type = phpstr.charAt(idx);
        idx += 2;
        return type;
      } //end readType

      ,
      readKey = function() {
        var type = readType();
        switch (type) {
          case 'i':
            return readInt();
          case 's':
            return readString();
          default:
            var msg = "Unknown key type '" + type + "' at position " +
              (idx - 2);
            throw new Error(msg);
        } //end switch
      }

      ,
      parseAsArray = function() {
        var len = readLength(),
          resultArray = [],
          resultHash = {},
          keep = resultArray,
          lref = ridx++,
          key, val, i, j, alen;

        refStack[lref] = keep;
        try {
          for (i = 0; i < len; i++) {
            key = readKey();
            val = parseNext();
            if (keep === resultArray && key + '' === i + '') {
              // store in array version
              resultArray.push(val);

            } else {
              if (keep !== resultHash) {
                // found first non-sequential numeric key
                // convert existing data to hash
                for (j = 0, alen = resultArray.length; j < alen; j++) {
                  resultHash[j] = resultArray[j];
                }
                keep = resultHash;
                refStack[lref] = keep;
              }
              resultHash[key] = val;
            } //end if
          } //end for
        } catch (e) {
          // decorate exception with current state
          e.state = keep;
          throw e;
        }

        idx++;
        return keep;
      } //end parseAsArray

      ,
      fixPropertyName = function(parsedName, baseClassName) {
        var class_name, prop_name, pos;
        if (
          typeof parsedName === 'string' &&
          "\u0000" === parsedName.charAt(0)
        ) {
          // "<NUL>*<NUL>property"
          // "<NUL>class<NUL>property"
          pos = parsedName.indexOf("\u0000", 1);
          if (pos > 0) {
            class_name = parsedName.substring(1, pos);
            prop_name = parsedName.substr(pos + 1);

            if ("*" === class_name) {
              // protected
              return prop_name;
            } else if (baseClassName === class_name) {
              // own private
              return prop_name;
            } else {
              // private of a descendant
              return class_name + "::" + prop_name;

              // On the one hand, we need to prefix property name with
              // class name, because parent and child classes both may
              // have private property with same name. We don't want
              // just to overwrite it and lose something.
              //
              // On the other hand, property name can be "foo::bar"
              //
              //     $obj = new stdClass();
              //     $obj->{"foo::bar"} = 42;
              //     // any user-defined class can do this by default
              //
              // and such property also can overwrite something.
              //
              // So, we can to lose something in any way.
            }
          } else {
            var msg = 'Expected two <NUL> characters in non-public ' +
              "property name '" + parsedName + "' at position " +
              (idx - parsedName.length - 2);
            throw new Error(msg);
          }
        } else {
          // public "property"
          return parsedName;
        }
      }

      ,
      parseAsObject = function() {
        var len, obj = {},
          lref = ridx++
          // HACK last char after closing quote is ':',
          // but not ';' as for normal string
          ,
          clazzname = readString(),
          key, val, i;

        refStack[lref] = obj;
        len = readLength();
        try {
          for (i = 0; i < len; i++) {
            key = fixPropertyName(readKey(), clazzname);
            val = parseNext();
            obj[key] = val;
          }
        } catch (e) {
          // decorate exception with current state
          e.state = obj;
          throw e;
        }
        idx++;
        return obj;
      } //end parseAsObject

      ,
      parseAsCustom = function() {
        var clazzname = readString(),
          content = readString('}');
        // There is no char after the closing quote
        idx--;
        return {
          "__PHP_Incomplete_Class_Name": clazzname,
          "serialized": content
        };
      } //end parseAsCustom

      ,
      parseAsRefValue = function() {
        var ref = readInt()
          // php's ref counter is 1-based; our stack is 0-based.
          ,
          val = refStack[ref - 1];
        refStack[ridx++] = val;
        return val;
      } //end parseAsRefValue

      ,
      parseAsRef = function() {
        var ref = readInt();
        // php's ref counter is 1-based; our stack is 0-based.
        return refStack[ref - 1];
      } //end parseAsRef

      ,
      parseAsNull = function() {
        var val = null;
        refStack[ridx++] = val;
        return val;
      }; //end parseAsNull

    parseNext = function() {
      var type = readType();
      switch (type) {
        case 'i':
          return parseAsInt();
        case 'd':
          return parseAsFloat();
        case 'b':
          return parseAsBoolean();
        case 's':
          return parseAsString();
        case 'a':
          return parseAsArray();
        case 'O':
          return parseAsObject();
        case 'C':
          return parseAsCustom();
        case 'E':
          return parseAsString();

          // link to object, which is a value - affects refStack
        case 'r':
          return parseAsRefValue();

          // PHP's reference - DOES NOT affect refStack
        case 'R':
          return parseAsRef();

        case 'N':
          return parseAsNull();

        default:
          var msg = "Unknown type '" + type + "' at position " + (idx - 2);
          throw new Error(msg);
      } //end switch
    }; //end parseNext

    return parseNext();
  };
}));