Flow Designer Scratchpad

“Necessity is the mother of invention.”
English-language proverb

I really should be working on testing out the latest enhancements to my Dynamic Service Portal Breadcrumbs right now, but I ran into this other issue recently, and I really want to see if I can make this work. I hate it when people start something and then move on to other things without ever finishing up what they started, so I definitely want to circle back and wrap that one up; however, today is not that day.

Today I want to talk about the Flow Designer. I have been striving to convert any of my old legacy Workflows over to the newer Flow Designer tool whenever the opportunity arises. The other day I was doing just that with a Workflow that made extensive use of the Workflow Scratchpad feature. When I went to look for the equivalent feature in the Flow Designer, I couldn’t find anything. I thought maybe that it wasn’t needed for some reason, so I tried several workarounds, but nothing worked. Nothing that I tried would preserve and/or modify data between or across Actions or Subflows. After quite a number of failed attempts to find something that would do the job, I eventually came to realize that if I wanted some kind of Scratchpad capability in the Flow Designer, I was going to have to build it myself.

My first thought was that all that I would need would be simple setProperty and getProperty functions, but then I realized that I would first need to establish the scratchpad, and once established, I would want to be able to get rid of it as well, so that turned into four relatively simple functions, which is still not too bad. When I say functions, what I really mean are Flow Designer Actions, but since I will be calling some function in a common Script Include built for this purpose, I still think of them as functions. Here is the function to create a scratchpad, which is just basically a record on a table that I created for this purpose:

createScratchpad: function() {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	spGR.initialize();
	spGR.setValue('u_scratchpad', '{}');
	if (spGR.insert()) {
		response.success = true;
		response.scratchpad_id = spGR.getUniqueValue();
		response.message = 'Scratchpad record successfully created';
	} else {
		response.success = false;
		response.message = 'Unable to create scratchpad record';
	}

	return response;
},

The scratchpad itself is just a JSON string stored in the only column added to the table, u_scratchpad. We initialize that to an empty object and save the record and that’s about all there is to that. To get rid of it, we will need to have the sys_id of the record, but there is not much code behind that process, either:

deleteScratchpad: function(spId) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		if (spGR.deleteRecord()) {
			response.success = true;
			response.message = 'Scratchpad record successfully deleted';
		} else {
			response.success = false;
			response.message = 'Unable to delete scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That takes care of building up and tearing down the scratchpad object. Now, to use it, we will need those setProperty and getProperty functions that we were talking about earlier. This one will let you set the value of a property on the scratchpad:

setScratchpadProperty: function(spId, propertyName, propertyValue) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		var jsonObject = {};
		try {
			jsonObject = JSON.parse(jsonString);
		} catch(e) {
			response.warning = 'Unable to parse JSON string: ' + e;
		}
		jsonObject[propertyName] = propertyValue;
		jsonString = JSON.stringify(jsonObject, null, '\t');
		spGR.setValue('u_scratchpad', jsonString);
		if (spGR.update()) {
			response.success = true;
			response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
		} else {
			response.success = false;
			response.message = 'Unable to update scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

… and this one lets you retrieve the value of a property on the scratchpad:

getScratchpadProperty: function(spId, propertyName) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		try {
			var jsonObject = JSON.parse(jsonString);
			var propertyValue = jsonObject[propertyName];
			if (propertyValue > '') {
				response.success = true;
				response.property_value = propertyValue;
				response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
			} else {
				response.success = false;
				response.message = 'Scratchpad property "' + propertyName + '" has no value';
			}
		} catch(e) {
			response.success = false;
			response.message = 'Unable to parse JSON string: ' + e;
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That’s it for the core functions needed to make this work. Putting it all together, the entire Script Include looks like this:

var SNHScratchpadUtils = Class.create();
SNHScratchpadUtils.prototype = {
    initialize: function() {
    },

	createScratchpad: function() {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		spGR.initialize();
		spGR.setValue('u_scratchpad', '{}');
		if (spGR.insert()) {
			response.success = true;
			response.scratchpad_id = spGR.getUniqueValue();
			response.message = 'Scratchpad record successfully created';
		} else {
			response.success = false;
			response.message = 'Unable to create scratchpad record';
		}

		return response;
	},

	setScratchpadProperty: function(spId, propertyName, propertyValue) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch(e) {
				response.warning = 'Unable to parse JSON string: ' + e;
			}
			jsonObject[propertyName] = propertyValue;
			jsonString = JSON.stringify(jsonObject, null, '\t');
			spGR.setValue('u_scratchpad', jsonString);
			if (spGR.update()) {
				response.success = true;
				response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
			} else {
				response.success = false;
				response.message = 'Unable to update scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	getScratchpadProperty: function(spId, propertyName) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			try {
				var jsonObject = JSON.parse(jsonString);
				var propertyValue = jsonObject[propertyName];
				if (propertyValue > '') {
					response.success = true;
					response.property_value = propertyValue;
					response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
				} else {
					response.success = false;
					response.message = 'Scratchpad property "' + propertyName + '" has no value';
				}
			} catch(e) {
				response.success = false;
				response.message = 'Unable to parse JSON string: ' + e;
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	deleteScratchpad: function(spId) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			if (spGR.deleteRecord()) {
				response.success = true;
				response.message = 'Scratchpad record successfully deleted';
			} else {
				response.success = false;
				response.message = 'Unable to delete scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	type: 'SNHScratchpadUtils'
};

Now that we have all of the functions, we need to turn those into Flow Designer Actions. Before we do that, though, let’s create a Category for them so that we can group them and they will be easy to find. We do that by adding a row to the Action Category table, sys_hub_category. With that out of the way, we can create our first Action, which will invoke the createScratchpad function in our Script Include.

Create Scratchpad Action

The entire Action is just a Script step that leverages our Script Include and passes the results on to the Action Outputs. The small script to make that happen is just a few short lines of code:

var snhspu = new SNHScratchpadUtils();
var result = snhspu.createScratchpad();
for (var key in result) {
	outputs[key] = result[key];
}

Now we just need to repeat that process 3 more times to create Flow Designer Actions from our three other Script Include functions and we’re all set. To test things out, there is a little Test button right at the top of the Flow Designer page, and for the Create Scratchpad Action, there isn’t even any input to set up, so you can just click that button and go. Once you test out the Create Scratchpad Action, you can snag the Scratchpad ID out of the Action Outputs and then use that as an input to test all of the others.

Well, that wasn’t so bad: one Script Include, four functions, one Action Category, and four Actions. I threw this together rather quickly, but here is the Update Set. If you run into any issues with that, or if you can think of any way to make it better, please let me know in the comments. Of if you know of a built-in function that eliminates the need for this, that would be even better!

Note: With the introduction of Flow Variables, this component is no longer necessary.