Setting up a Text-to-Workflow Entry Workflow

  • By Michael Garrison One Year Ago

At RX2017, the Spark/Minecart team announced that, due to the larger-than-expected demand (and ministry usefulness) of the Text-To-Workflow plugin, they were reducing the price for the plugin from $600 to $0, and when v7 is released, it will simply be a part of Rock rather than being a plugin.

Now that's ministering to the "Big C" church!

They also used the plugin to great effect to create "takeaways" for conference-goers. These factors together meant that interest in getting up and running with the plugin went sky-high.

One recurring question continued to be "How do I handle figuring out if a person is in Rock, and if not, getting their information so that they are?". My church had actually spent a week or so working together to handle that ourselves, and when I shared our approach, several people asked me to document what we'd done. Thus, this article.

Thanks to the plugin's great design, it's actually pretty easy to tell whether the person who has texted you is in Rock. Since the plugin passes a person attribute to your workflow if the phone number matches anyone's phone number in Rock, all you have to do is map that attribute and then check to see if it's blank. If it is, they're not in Rock. If it's not blank, they are in Rock- you've got a person attribute you can use to address them properly, update their record, etc.

So that's not the issue. The issue is in getting their information and storing it as a person record in Rock if they're not already there. (Or if they are in Rock, just not with a matching phone number). It ends up being a lot of actions at the beginning of any TTW workflow, which are a pain to re-create for every new keyword workflow.

So our team created this one universal workflow that does all of that heavy lifting. Every keyword triggers that one workflow, rather than their own individual workflows. At the end of our universal T2W entry workflow, we will look at the keyword ourselves and then pass the flow to the workflow type we actually want to handle the keyword in.

For example, if you want people to get a basic info text and be put into a group when they text in the word "camp", we will first activate our universal entry workflow. Once the person is matched or created in Rock, that workflow will actually activate our "camp" workflow. Likewise, if we want another keyword, say "first", that keyword will also activate our universal entry workflow, and once the person is matched or created in Rock, THEN our "first" workflow will be triggered.

Here's a graphical representation of the flow, if every workflow had to do it's own checking:


And here's what the flow looks like with this "entry" workflow:


Now, I know that the two lines in the entry workflow look pretty small, like it's not a big deal to just include them in each individual workflow. But that's the abbreviated version, noting what it does, not how it does it. A more action-by-action version of that workflow (and thus, the actions you'd need to include at the top of each individual workflow would actually look more like:

  • Start a timer so that if they don't respond within a given timeframe, the conversation is deleted
  • Check and see if the person is in Rock
  • If not:
    • Send them a response and a personalized link asking them to provide their info
    • Present a User Entry Form for them to provide First Name, Last Name and Email address
    • Match or create a person in Rock
    • Update that person record with their phone number

So yes, you can do this in every workflow, but that's a lot of work when you want something simple like "send them a response". The way we're going to set up allows you to start each new keyword's workflow knowing that you've already got a valid person in Rock.

Here's how that might look. Remember that if the person is already in Rock, this workflow is going to complete very quickly and hand them right off to your secondary keyword-specific workflow. If they're not, it's going to pause and wait for them to tap on the link and provide you information before the second workflow (second message in this example) is launched.


OK, let's jump in.

Text-To-Workflow setup

In the "Admin Tools -> General Settings" menu, click on "Defined Types" and find "Text To Workflow". Add a new value to this list. Use the +1-formatted phone number you want to use for Text to Workflow (with no dashes) as the "Value". Note in the description that this takes all keywords and makes sure that a person is tied to the phone number. Leave the keyword expression blank, so it matches any messages sent to the phone number you provided. Use whatever template you'd like for the Workflow Name - something like "Message from: {{ FromPerson.FullName }}" works pretty well.

Now under Workflow Attributes, you can map the attributes into your workflow as noted in the documents. However, I only map three:

Attribute Key Merge Template
FromPhone {{ FromPhone }}
Message {{ MessageBody }}
FromPerson {{ FromPerson.PrimaryAlias.Guid }}

You're sharp, so I know you saw that the Merge Template I'm using for FromPerson isn't the same as the docs say to use. We are passing an actual person object to the Workflow, rather than just the name coming through like the Text to Workflow docs note. We could do this by grabbing initiator as well (per the docs), but this saves a step :)

You can also use MessageBody as the Attribute Key like the docs show you to do, just be sure to match the Workflow Attribute to that key once we get there. I set it up using the key of "Message" though, and I don't want to cause confusion by not having my screen shots match up.

We'll have to come back and set the workflow type once it's created, but for now hit Save and let's move on to the workflow!

Workflow Setup

Give your workflow a name, set it to "Active" and "Automatically Persisted". I set the Work Term to "Message", though that's up to you. I also put it in a workflow category we called "Text to workflow", but you can put it wherever it works for your church. Using an icon of fa fa-group might make sense, or you can leave it at the default fa fa-list-ol. Logging level of "None" is good.


You need at least 8 Workflow Attributes. Remember we mapped FromPerson (a Person type attribute), FromNumber (a Text type attribute I made required) and Message (a Text type attribute) from the Text-To-Workflow plugin. If you need the other attributes (date, etc) you can add those as well per the docs. So those are the first three Workflow Attributes we need, but we'll need a few more as well. Here's all of them:

Attribute Name Attribute Key Attribute Type
From Phone FromPhone Text
Message Message Text
From Person FromPerson Person
First Name FirstName Text
Last Name LastName Text
Email Address EmailAddress Email Address
Location Location Campus
ShortURL ShortURL Text

You can rename "Location" to "Campus" if you'd like, just make the change accordingly as you build out the activities. Additionally, if you don't want people to select their campus (or especially if you only have one), you can set a default for that attribute so it's pre-filled for them.

Create and name the activities

Since we're going to be creating a lot of "Activate Activity" actions, I find it helps to have the activities already created and named ahead of time. Here are what I called the 5 activities we need:

  • Determine if they're in Rock
  • Generate Short URL
  • Create or Match Person
  • Pass Message to External Workflow
  • Stale workflow if no response


Determine if they're in Rock

This first activity should be set to Activate With Workflow.

Add the first action. Then click the filter button at the top right of the action to bring up the yellow "Run If" bar. Set this action to run if FromPerson and choose Is Not Blank

I called this action "Already in Rock; Pass directly to another workflow", and as you can see, it's only going to run if Text To Workflow gave us a person object. So set Action Type to "Activate Activity" and select "Pass Message to External Workflow".


Add the second action. Once again, click the filter button at the top right of this action to bring up the yellow "Run If" bar. Set this action to run if FromPerson and choose Is Blank.

I called this action "Not in Rock; Start getting person" – it will run if Text To Workflow could NOT find the person who sent the message. So set Action Type to "Activate Activity" and select "Generate Short URL".


Now, you may think we're done with this activity. But I like adding one more feature to keep things realistic. If someone sends us a message and they're not in Rock, they're going to get asked to provide their information. If they decide not to and we never hear from them again, now what? Do you want to leave this workflow active forever? Never do anything with their request? If not, we need to do something once we decide they're not going to give us the info.

So add a third action to the first activity. I called it "Stale after given time". Set the Action Type to "Activate Activity", and choose "Stale workflow if no response". Now this is important: be sure to tell this action that it should complete the activity on success. Because if they're not in Rock, the first action isn't going to run. So the activity isn't going to auto-complete, meaning that first action will be evaluated every time the workflow is processed. At some point, "FromPerson" IS going to be filled in (once they fill out the form and we match or create a person record for them, at which point that first action will actually run and activate the specified external workflow. We need to handle that elsewhere, so by marking this activity as complete, that first action will no longer be evaluated.


Generate Short URL

I'll admit it. I can be security-paranoid in some areas…and that trait is the entire reason for this activity. Ultimately, we need to be able to send people who aren't already in Rock a link that will let them fill in their information. That link could look like , where 6 is the WorkflowId of the workflow we kicked off when they texted in. But … I don't trust someone to not decide to see what /info/5 and /info/7 contained then, and where they might find someone ELSE'S unfilled workflow, and decide to fill in bogus information which would now be associated with that other person's phone number. We could use the Workflow GUID in the link instead, but that's way too long of a link to look nice in a text. So I created this activity to generate a short random URL (kind of like might). It takes some extra workflow processing, and an extra page so we can translate that short url back to an actual workflow. But I like the process of turning /info/sjr63f into /page/xxx?WorkflowGuid=123456789-1234-1234-1234-12345678 ourselves, without most people even noticing (since they're on their phones and the full URL rarely shows).

If you're not worried about sending /info/6 type URLs, you can actually delete this activity, skip this section, and change the "Not in Rock; Start Getting Person" action in the first activity to "Create or Match Person" instead of "Generate Short URL".

OK, here we go with the second activity

First, create an activity attribute called "Count". I just set it to a text type field.


Now let's get moving with the actions. The first one I called "Generate Random". It's a SQL Run action. The query is SELECT RIGHT(NEWID(),6) . Sure, there are other ways of generating random characters, but this is awfully efficient, and as a bonus it won't include letters above F, so there aren't any ambiguous characters like O or I. Store the Result Attribute in our Short URL attribute.


Now, we need to make sure that the short URL we chose is actually unique among other instances of this workflow. I called the second action "Check Uniqueness". It's another SQL Run action…with a longer query this time:

	[AttributeValue] av
	INNER JOIN [Attribute] a ON a.[Id] = av.[AttributeId]
	LEFT JOIN [Workflow] w ON w.[Id] = av.[EntityId]
	a.[EntityTypeId] = 113					--Workflow
	AND a.[EntityTypeQualifierColumn] = 'WorkflowTypeId'	--Better safe than sorry
	AND a.[EntityTypeQualifierValue] = 34			--WorkflowTypeId
	AND w.[Status] = 'Active'
	AND a.[Key] = 'ShortURL'
	AND av.[Value] = '{{ Workflow | Attribute:'ShortURL' }}'

Change the WorkflowTypeId line, replacing "34" with the ID number of your actual workflow type. The other values (113, WorkflowTypeId, etc) should be fine as they are, as long as your Short URL attribute has a key of ShortURL (capitalization matters!). Store the result of that query in our "Count" attribute.

All this does, is count the number of active workflows where the ShortURL attribute has the same data as our current Workflow. Usually, it will find only 1 (the current workflow itself). But it may find 2, if it happened to choose a random string that it had chosen before.


So our third action needs a "Run If" filter, so that it will Run If Count is Greater Than 1. The action should be set to "Activate Activity" and you should select "Generate short URL" as the activity to activate. Finally, be sure to tick the box to mark this activity completed on success, so it doesn't go on.


Our fourth action will therefore only run if exactly one workflow was found with a matching ShortURL. This action will be another "Activate Activity", but this one should activate "Create or Match Person". As with the other action, this action needs to mark the Activity completed on success.


Create or Match Person

Now that we have a short random string that we know can identify this workflow reliably, we need to use it! The first action in the third activity is finally going to ask them to give us some more information (remember this only runs if they're not already in Rock – if they were they skipped right from activity 1 to activity 4). So make this action a "SMS Send" action type. Set it to be sent from your T2W Twilio number, and have it be sent to "FromNumber" (which was auto-assigned by T2W). The message is up to you, but I recommend something like Thanks for reaching out! Unfortunately it doesn't look like we've heard from you on this number before - would you please provide some basic information so we can connect you correctly?{{ Workflow | Attribute:'ShortURL' }}.

Note that if you opted to skip the "Generate Random" activity, you could replace that last bit of lava in the link with something like {{ Workflow.Id }} instead.


We're going to cover that /info/ page nearer the end of the article, just trust me that when they go there, they're going to end up on a WorkflowEntry page with this workflow loaded. So…let's give them something to look at!

The second action in this activity is a User Entry Form. As I'm sure you know by now, Rock is designed to format pages - even forms like this - nicely on mobile devices. So we're just going to build a form to have them provide their information that we want to store in their person profile and associate with this phone number. I called this action "Gather Data", but they never see that name so you can call it anything. Set it to a User Entry Form action type, and set Notification Email to "None". All we want in the form is "First Name", "Last Name", "Email Address" and possibly "Location" (or Campus, if you named it that way). All 3 or 4 should be visible, editable, and required. Nothing else should be visible. You can change the Response Text if you wish, but otherwise, everything else on the default form should be good to go. Remember that a person record is required to have an associated Campus identified on it, so if you don't have them choose the value to be associated with their person record, you'll need to set a default in the attribute.


Now we have their name and email address. Let's find (or make) their person record! Add a "Person Attribute From Fields" action (I called it "Match Person". The First Name should be set to the Attribute Value of your First Name attribute. Likewise the Last Name should be your Last Name attribute, and the Email Address should be your Email Address attribute. Set the Person Attribute to your "FromPerson" attribute, and you can set the record status and connection status as you wish. Finally, set the Default Campus field to your "Location" (or "Campus") attribute. Yay! Now you have a person record, whether it is new or already existed!


Wait…but the T2W still doesn't know it, because the record wasn't created with their phone number already attached. No problem. Create a fourth Action in this Activity called "Add phone number to person". The action type is "Person Phone Update". The person attribute should be set to your "FromPerson" attribute. For the Phone Type, you can just select "Mobile" from the dropdown (ignore the Phone Type From Attribute field). The Phone Number should use the attribute value of FromNumber. Leave "unlisted" blank, and type the word Yes under "Messaging Enabled" (they've established the communication channel by contacting you via text first, which I take as indication they don't mind being texted back). You can leave "Ignore Blank Values" set to "Yes" – we know the number won't be blank.


Finally, we have our person record. Now we can activate the correct external workflow to correspond with the keyword they sent us, just as if they'd already existed when the workflow started. Add the fifth and final Action to this activity, and set the action type to "Activate Activity". Select "Pass message to external workflow" as the activity to activate.


Pass Message to External Workflow

This is where we're going to actually look at the keywords and figure out which workflow to launch. So I'm going to give you an example here, and you're going to create as many copies of this one action for every keyword you want to have. For now, that means that when you want to add a new one you'll need to edit this workflow to add a new action to this activity. (Check out the Future improvements section at the end of this article for other possible ways of matching and activating external workflows with keywords, that don't involve editing this long workflow).

But first, there's one thing we need to do with the message they sent you - we're going to match workflows based on what keywords they contain, and that's case-sensitive. So we need to change the body to all lowercase characters, so that whether they send 'new', 'New', or 'NEW', we'll check for a lowercase match using 'new'.

Create the first action in this activity as an Attribute Set Value action. Set the value to {{ Workflow | Attribute:'Message' | Downcase }}


Now we can start figuring out which external workflow of yours to pass the flow to. Let's say you want to respond to a keyword of "new". Make an action (I'd call it "new") that will Run If Message Starts With new (remember to click the filters to get the "Run If" options). The Action Type should be "Activate Workflow". You're probably going to want to pass at least the Message and FromPerson attributes to the new workflow, so click the black (+) button and set both Message and FromPerson to be populated by their respective attribute values from this workflow. Give your Workflow a name (if you rename it in the workflow you activate it, it'll overwrite this value so don't worry about it too much), and select the type of workflow you want to activate. Easy!

(Note you could also use "Contains" or even a regex match, rather than "Starts with").


So create an activity similar to this for every keyword you want to activate a workflow for. And it'll get activated as soon as Rock has a person record it can associate with the incoming number.

Before we move on from this fourth activity, we need to add one final action. It's important this always stays at the end, so that once your external workflow is activated, this workflow closes.

The last activity should be of type "Workflow Complete". That's it- once this runs, everything will stop being processed in the future.


Stale workflow if no response

Finally, the last activity. This activity gets triggered almost as soon as the workflow starts, and mostly does nothing. But after a certain timeout, if the workflow hasn't been completed (by activating an external workflow in activity #4 above), it's going to assume that you're not going to hear from them, and close this workflow. That will free up resources since this workflow won't need to be processed any longer, and also free up the random ShortURL string that we "claimed". (It also ensures that the Entry Form doesn't remain open to be found somehow and get filled in with bogus data years from now).

So the first action is going to be a "Delay". I set our delay to 20160 minutes (14 days), but you can adjust as your needs dictate. Perhaps you expect people to take longer, or shorter, and still be able to fill out that easy form. Tweak away. Remember, this is running in parallel with your other active activities, so all it's "delaying" is the action which comes next.


…which brings us to the very last action in the workflow: one final "Workflow Complete" action. So after 14 days, this will run. Once it does, this workflow will no longer be processed, the form will not be found (or available) using the short URL or any other address, and you still won't have a name associated with this number (though this completed workflow will remain as a record that they did text you).


You could also replace this last action with another reminder SMS along the lines of "We haven't heard from you! Please fill out the form (link)", or read their message and let someone on staff know that this number was interested in [message] but we couldn't find out who they are, and ask them to follow up manually, etc.

Custom Pages

OK, now let's make sure that /info actually does what we need it to.

Create a new page (probably on your external site), and give it a Page Route of info/{Key} Now add a block to the Main zone of the page – a Dynamic Data block

Here is the query for that block (note that since we're taking unsanitized input from the url, rather than using an @parameter, I'm sterilizing the input in the first line and writing the value with Lava):

{% if PageParameter.Key %}{% assign Key = PageParameter.Key | Replace:"'",'' | Replace:'--','' %}{% else %}{% assign Key = 0 %}{% endif %}
    TOP 1 w.[Guid]
    [AttributeValue] av
    INNER JOIN [Attribute] a ON a.[Id] = av.[AttributeId]
    LEFT JOIN [Workflow] w ON w.[Id] = av.[EntityId]
    a.[EntityTypeId] = 113                                  --Workflow
    AND a.[EntityTypeQualifierColumn] = 'WorkflowTypeId'    --Better safe than sorry
    AND a.[EntityTypeQualifierValue] = 34                   --WorkflowTypeId
    AND w.[Status] = 'Active'
    AND a.[Key] = 'ShortURL'
    AND av.[Value] = '{{ Key }}'

Replace the WorkflowTypeId in this query (34 in the example) with the actual ID of your workflow type. An easy way to get this number is to look at the address bar when you're viewing/editing your workflow - it lists WorkflowTypeId= there.

Formatted output:

{% for row in rows %}
    {% capture theUrl %}https://yourdomain.tld/page/123?WorkflowGuid={{ row.Guid }}{% endcapture %}
    {{ theUrl | PageRedirect }}
{% endfor %}

Now create a child page of this page and take note of the ID number. Replace the domain and the page number (123) in the Formatted Output with your actual values. Then go to the child page you just created.

On that child page, add a WorkflowEntry block to the main zone. Get into its settings and configure it to point only to your T2W entry workflow type, so all you need to provide it is the Workflow Guid.


Now when someone hits (for instance) /info/af3748 the DD block will look for any workflows of the same workflow type where ShortURL=af3748, and redirect them to a url like /page/123?WorkflowGuid=12345678-1234-1234-12345687. Since the Workflow Entry block already knows what type of workflow it should be displaying, it just needs the GUID to know which workflow's form to display. We could use ID instead, but like I said, I prefer random identifiers being exposed to people, rather than numerical incrementing ones.

Wrapping up:

OK, now the last step is to go back to the Text To Workflow defined types and point the hook you created at this workflow you're done with. If the pattern is left blank, it will send all texts sent to that number to this workflow, so now you've got an effortless way of making sure that everyone texting in gets matched with their person profile.

That's about it! Now you can build workflows that match keywords and start knowing that right from the beginning you've got a person record to work with.

Thoughts on future improvements:

It's not ideal to have to come in and edit this workflow every time you want to add a new keyword. NewSpring has looked at creating a Content Channel type that takes a workflow type as an attribute, so you can link keywords to workflows to launch. They said it worked, but was really slow. Perhaps we'll hear more about that later on.

It seems to me though that it might make sense to create our own "Defined Type" (just like the Text To workflow defined type) where we can map keywords to a workflow type in the same manner as Text to Workflow expects to be used. I think that's going to take SQL to look up though, and I'm not sure whether to use "Begins with", "Contains" or really bite the bullet and set up a regex match like Text to Workflow does. When I have progress on something that works, I'll update this article. But hopefully having to add an action to activity 4 every time we want to add a keyword is only a temporary situation.

I'd love to hear your thoughts on these or other methods, and now that we have comments on this site, feel free to jump in and share!

Spark Development Network
Flagstaff, AZ

Michael Garrison recently left his job in Architecture to become one of the "new guys" at Spark (the "Core Team"), but he's still helping out Christ's Church of Flagstaff and other non-profits with tech needs in his off-hours, trying to make computers do what computers do best, so that people are freed to do what we do best: relate with people!