Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Use Subflow EVERYWHERE

21 Nov 2022 🔖 salesforce flow integration intermediate tips web development
💬 EN

Table of Contents

Flownatic Salesforce admins! Have you ever let a Salesforce Developer (like the ones at UnofficialSF) write complicated Apex so you could call it in your Flow as an Action?

What if I told you that developers could depend on your flows instead when they need to do something complicated?

Salesforce developers who’ve read Andy Fawcett’s book “Salesforce Lightning Platform Enterprise Architecture - Third Edition ” should be familiar with a programming concept Andy helpfully re-shared called the “application service layer” pattern, but I don’t think a lot of admins have heard of it yet.

Flow experts – the service layer programming pattern works perfectly with Auto Launched Flow!


The essence of a service

Anyone who’s built an Autolaunched Flow and run it from a different Flow with Subflow is well on their way to knowing how to build out an Application Service Layer, but not all Subflows are inherently Application Service Layers.

It’s like baking a cake. If you’ve ever made a cake, you’re well on your way to knowing how to make a strawberry cake. But if you don’t actually put strawberries into it, it’s not a strawberry cake!

Key Takeaway: If you want to make an Autolaunched Flow behave like a Service, then you need to make sure it doesn’t make any assumptions about how the rest of the world will run it.

Sounds simple, right?

Maybe, but there are subtleties to getting it right.

Let’s do a deep dive into the details of what’s allowed – and not allowed – when we want to design Autolaunched Flows to behave like “services.”


Rule: Use specialized input variables

The very first thing you should do when you open a brand new empty Autolaunched Flow is add some new Variable-typed resources to it that have the Available for input checkbox checked under Availability Outside the Flow.

Make heavy use of these Data Types for the input variables:

  1. Text
  2. Number
  3. Currency
  4. Boolean
  5. Date
  6. Date/Time
  7. Picklist
  8. Multi-Select Picklist
  9. Apex-Defined

Stay away from these Data Types for the input variables:

  1. Record

Warning

When I say the complex Autolaunched Flow masterpiece you’re about to build is going to be used “everywhere,” I mean everywhere.

  • Maybe you’ll run it as a subflow from an Actions and Related Records-typed Record-Triggered Flow whose Object is Lead.
  • Maybe you’ll run it as a subflow from an Actions and Related Records-typed Record-Triggered Flow whose Object is Contact.
  • Maybe you’ll run it as a subflow from an Actions and Related Records-typed Record-Triggered Flow whose Object is Account.
  • Maybe you’ll run it as a subflow from a Schedule-Triggered Flow that loops over a bunch of Lead, Contact, and Account records daily at midnight.
  • Maybe you’ll run it as a subflow from a Screen Flow that helps your Salesforce power users decide how best to do their jobs.
  • Maybe developers will embed a form in your company’s public website’s “find help” page that runs your Autolaunched Flow.
    • (I told you you’d have developers coming to you for help!)

Back to that “cake” analogy – if you’ve ever built an Autolaunched Flow, you probably have that experience because you’ve probably already thought about reusability. As Melody said at Salesforce Flowsome:

“The most common case is when you have many flows and in each of them there is the same set of actions, you can make that set of actions into a Subflow so you do not need to create those actions again and again.”

To add strawberries to our mental model and start “thinking in services,” we need to think about reusability in an even more extreme fashion.

That’s why I’ve laid out rules like staying away from certain data types.

If your Autolaunched Flow isn’t generic enough, you can’t reuse it widely enough.

Example

If you’re building an Autolaunched Flow that takes a “country of the world” as input and returns a “user assigned to helping people in that country” as output, do not let your Autolaunched Flow make any assumptions about how it got the name of the country.

Maybe you know that it will always come from Account.BillingCountry, Lead.MailingCountry, or Contact.MailingCountry, but don’t let your AutoLaunched Flow know that!

Just give your Autolaunched Flow an Available for input variable that’s called “Country” with a data type of Text and leave it at that.

Pineapple cake

Remember that the Autolaunched Flow you’re creating to behave like Andy’s idea of an “Application Service Layer” can itself call other Autolaunched Flows as Subflows.

It’s okay for those “deeper” Autolaunched Flows to use input variables with data types like “Record,” if that helps you feel organized and tidy.

Just don’t use “Record” as a data type for the Autolaunched Flow you’re trying to offer up as a “Service” that you can “use everywhere.” It’s not general-purpose enough.


Rule: Use specialized output variables

The very second thing you should do, after creating some input variables, is add at least 1 new Variable-typed resource with the Available for output checkbox checked under Availability Outside the Flow.

Tip: It’s not against the rules for this to be one of the same variables that’s marked “Available for input”, but when in doubt, start by building your Autolaunched Flow with separate input and output variables.

You can use any Data Type, but don’t forget to create give the Autolaunched Flow least 1 output variable if you’re trying to make it behave like a “service.”


Rule: Avoid Editing Data

To give your Autolaunched Flow that strawberry “service layer” flavor, make as much use as you’d like of these Flow elements:

  1. Logic: Assignment
  2. Logic: Decision
  3. Logic: Loop
  4. Logic: Collection Sort
  5. Logic: Collection Filter
  6. Data: Get Records
    • You’ll almost certainly have to make heavy use of Get Records if all of your input variables are Text, Number, Currency, Boolean, Date, or Date/Time and you followed my rule of not letting input variables be the Record data type.
    • You might be able to avoid Get Records, and hence speed up your Autolaunched Flow, by passing it richer inputs in the first place (yet not relying upon the Record data type), if you partner with a developer and set input variables to the Apex-Defined data type.

But stay away from these:

  1. Data: Create Records
  2. Data: Update Records
  3. Data: Delete Records
  4. Logic: Pause

And exercise great caution with these – only use ones that aren’t just going to accidentally stumble you into the “stay away from these” elements:

  1. Interaction: Action
  2. Interaction: Subflow

Warning

Remember what Melody said about Subflows and reusability in general:

“When you have many flows and in each of them there is the same set of actions, you can make that set of actions into a Subflow so you do not need to create those actions again and again.”

And then remember what I said:

“When I say the complex Autolaunched Flow masterpiece you’re about to build is going to be used ‘everywhere,’ I mean everywhere.”

Record-Triggered Flows from a variety of object types, Schedule-Triggered Flows, Screen Flows, your company’s public website, etc.

If your Autolaunched Flow isn’t generic enough, you can’t reuse it widely enough.

Example

If you’re building an Autolaunched Flow that takes a “country of the world” as input and returns a “user assigned to helping people in that country” as output, do not let your Autolaunched Flow actually set that User ID as the OwnerID of a record.

Maybe you know know that you always want to assign Account.OwnerId based on Account.BillingCountry, but your strawberry-cake “service” Autolaunched Flow shouldn’t know that.

Just give your Autolaunched Flow an Available for output variable called “Suggested_User” with a Data Type of Record and an Object of User and leave it at that.

Let the Record-Triggered Flows, Schedule-Triggered Flow, Screen Flow, etc. decide what to do with the User that your Autolaunched Flow suggested.

Before-save

A great reason to keep data modification out of your “service”-style Autolaunched Flow is that the calling context might be better at data modification than your Autolaunched Flow could ever be.

For example, if Salesforce ever starts allowing Subflow from Fast Field Updates-typed Record-Triggered Flows, it’d be a huge performance loss to try to have your Autolaunched Flow do an explicit “Update Records” against {!Some_Account_Typed_Variable.OwnerId} instead of just letting the Record-Triggered Flow against Account do an “Assignment” to {!$Record.OwnerId}.

  • Prepare yourself for that beautiful day and let an Actions and Related Records-typed Record-Triggered Flow against Account do the “Update Records” against {!Some_Account_Typed_Variable.OwnerId}.
    • Then, if Salesforce ever releases the feature, all you have to do is replace the Record-Triggered Flow that runs your “country to user suggestion” Autolaunched Flow as a Subflow – without actually editing the contents of the Autolaunched Flow itself.
  • Or give yourself the future today. Mitch Spano’s Trigger Actions Framework lets you use Subflow in a Fast Field Update situation, despite it not being available natively in Salesforce.

Web form

Remember that form I said a developer would be embedding on your company’s public website’s “find help” page? Picture this: the “form” is a map of the world, and as visitors click on countries, they see the first and last name of the User in charge of helping them.

In the case of a website using your Autolaunched Flow to do the “country to User” calcluation, there’s no need to edit any data inside of Salesforce just because someone clicked a map.

For the website, your Autolaunched Flow is just supporting a bunch of “What if?” exploration.

There’s no data-modification needed at all!

Recap

Remember: If you’re trying to design an Autolaunched Flow to behave like a Service, you need to make sure it doesn’t make any assumptions about how your Autolaunched Flow is going to be run.

Let the things that use your “service”-style Autolaunched Flow worry about doing things like modifying data in your Salesforce org.

Don’t put data modification into your “service”-style Autolaunched Flow itself.

(At least not until you’re very experienced with thinking in Application Service Layers and can judge with 100% certainty that it’s absolutely positively impossible that anyone could ever come up with any sort of read-only use for your Autolaunched Flow like the map-of-the-world website.)


Hands-on project

Spin up a scratch org from my sf-flow-service-layer codebase (instructions in the link).

Open up the scratch org in your web browser, go to Setup, and note that the Autolaunched Flow labeled “Sales Rep Suggester” isn’t very fancy.

Screenshot of the Sales Rep Suggester subflow -- it has six branches, first two, then each of those two into three.

No matter which branch of the “Sales Rep Suggester” Flow that data passed into it goes down, I currently have each of the 6 Get Records operations in the “Sales Rep Suggester” Flow programmed to always suggest the same old pattern when picking a User:

  • The first one found whose Id is not blank.
    • Filter User Records has 1 condition where field Id Does Not Equal Empty String (Not Null)
    • How Many Records to Store is set to Only the first record.

Your scratch org was probably created with the following users:

  1. A Chatter Free User named Chatter Expert
  2. An Analytics Cloud Integration User named Integration User
  3. An Analytics Cloud Security User named Security User
  4. A System Administrator named User User

Task 1: Add users and edit the Flow to assign to them

Edit the Sales Rep Suggester subflow

  1. In your web browser, in your scratch org, add additional Users. Create two more with the User License type “Salesforce Platform,” the ProfileStandard Platform User,” and the “Generate new password and notify user immediately checkbox unchecked. Name them “Anush Amjit” and “Benita Borges.”
  2. Open the Flow labeled “Sales Rep Suggester” for editing.
  3. Edit the Get Records component whose label is “FT APAC/LATAM rep” and whose ID is “Get_sales_rep_user_for_first_time_APAC_LATAM”. Change the condition under Filter User Records to be that field LastName Equals Borges.
  4. Change the condition within the other 5 Get Records components to be that field LastName Equals Amjit.
  5. Save a new version of the Flow.
  6. Click the Debug button toward the top of the Flow editor.
  7. Fill in Country_of_Residence as Japan and check the First_Time_Customer checkbox and click the Run button.
  8. At the right under Debug Details, expand “Get Records: FT APAC/LATAM rep” and confirm that the step’s output looks like this, with a “successfully found records” note:
     Find all User records where
     LastName Equals Borges
     Store those records in {!Sales_Rep_User}.
     Save these field values in the variable: Id
     Result
     Successfully found records.
    
  9. Click the Debug Again button toward the top of the Flow editor.
  10. Fill in Country_of_Residence as China and uncheck the First_Time_Customer checkbox and click the Run button.
  11. At the right under Debug Details, expand “Get Records: Rtng EMEA/other-region rep” and confirm that the step’s output looks like this, with a “successfully found records” note:
     Find all User records where
     LastName Equals Amjit
     Store those records in {!Sales_Rep_User}.
     Save these field values in the variable: Id
     Result
     Successfully found records.
    
    • Note: China went down the “other countries” branch of Returning Customers instead of “APAC” because the “Sales Rep Suggester” Flow includes a formula called “Country_to_Region” whose body is, at the moment, simply:
        CASE({!Country_of_Residence}, 'Japan', 'APAC', 'Kenya', 'EMEA', 'Montserrat', 'LATAM', 'NOT_FOUND')
      
  12. Activate your new version of the Flow.

Test your Subflow edits with real Account records

  1. Create a new Account record named “Boiron Tokyo” and give it a Billing Country of Japan.
  2. Validate that it gets automatically assigned to be owned by Benita Borges, thanks to the existence of a Record-Triggered(-ish) Flow called “Account Assign Sugg Sales Rep Flow (TAF),” which calls “Sales Rep Suggester” as a Subflow.
  3. Create a new Account named “Dell China” and give it a Billing Country of China.
  4. Validate that it gets automatically assigned to be owned by Anush Amjit, thanks to the existence of a Record-Triggered(-ish) Flow called “Account Assign Sugg Sales Rep Flow (TAF),” which calls “Sales Rep Suggester” as a Subflow.
    • (Note: When I wrote “Account Assign Sugg Sales Rep Flow (TAF),” to save myself time, I set it to force “First_Time_Customer to {!$GlobalConstant.True} even while I set Country_of_Residence to {!record.BillingCountry}, so there isn’t currently a way to test the “returning customer” branch of “Sales Rep Suggester” by manipulating real Account records.)

Test your Subflow edits via the API

Take a look at the URL of the website you’re on in Salesforce.

It’s probably something like:

  • https://12345.scratch.lightning.force.com/... or
  • https://12345.scratch.my.salesforce.com/...

Make note of what actually appears in your URLs instead of “12345”.

Then visit this URL, of course replacing “12345” with your value from your scratch org (note that it’s “site.com” not “salesforce.com“):

https://12345.scratch.my.site.com/services/apexrest/get_sales_rep?firstTimeCustomer=True&country=Japan

Validate that the contents of this website look something like this (should contain a Salesforce user ID starting with “005“):

<response>005987123654456987</response>

In fact, make note of exactly which user ID number it says. (If you really want to be thorough, validate that it’s Benita Borges’s user ID. It should be.)

Now visit this website (substituting 12345 as before):

https://12345.scratch.my.site.com/services/apexrest/get_sales_rep?firstTimeCustomer=False&country=China

Did the user ID starting with “005” change?

Great!

(If you really want to be thorough, validate that it’s Anush Amjit’s user ID. It should be.)

Note that you shouldn’t even need to open these “site.com” URLs in the same browser that’s already logged into your scratch org.

Theoretically, you could visit it from a totally different browser in incognito mode and it would work, because it’s a publicly available URL for an “API” included with this codebase.

(See “1: Build Your Own ‘Endpoint’ Inside Salesforce” in my article “Salesforce REST APIs: A High-Level Primer” for more details.)

In fact, if you want to try repeating these instructions in an incognito tab, go ahead!

You should get the same results.

Apex unit tests would normally need updating

In real life, the Apex class Sales_Rep_Suggester_Flow_TEST would have unit tests that are a lot more tightly coupled to the actual configuration of the “Sales Rep Suggester” Autolaunched Flow, and would need to be updated by an admineloper / developer before changes to “who gets which account” logic of “Sales Rep Suggester” could be deployed into production.

In this particular codebase, I just have all of the unit tests within Sales_Rep_Suggester_Flow_TEST validating that the user ID coming back from “Sales Rep Suggester” is not blank, so running the unit tests still pass.

That’s probably not a very good unit test for enterprise workloads, but I think it suffices for a tutorial project.

Takeaway

How cool is it that you were able to update nothing but the “Sales Rep Suggester” Flow, and instantly, both a Record-Triggered(-ish) Flow and an API started behaving according to your edits? #AwesomeAdmin

Task 2: Reuse the Subflow on Contact inserts

Can you create a new Flow that usesSales Rep Suggester” as a Subflow so that:

  1. If you create a new Contact who lives in Japan or Montserrat, Benita owns it?
  2. If you create a new Contact who lives anywhere else, Anush owns it?

Don’t do any edits to “Sales Rep Suggester” – that’d be cheating.

Easy way

Work

Create a new Record-Triggered Flow labeled “Contact Assign Sugg Sales Rep Flow (RTF)” with Object set to Contact, Trigger the Flow When set to A record is created, and Optimize the Flow for set to Actions and Related Records.

Add an Subflow element to the Flow. Choose “Sales Rep Suggester.” Label it “Get suggested owner user” and give it an API name of “Get_suggested_owner_user.”

Toggle both Set Input Values entries to “Include(the icons should slide to the right, be blue, and have a checkbox in them).

  1. Set Country_of_Residence to {!$Record.MailingCountry}.
  2. Set First_Time_Customer to {!$GlobalConstant.True}.

After the Subflow, before End, add an Update Triggering Record element to the Flow. Label it “Assign suggested user ID as record owner ID” and give it an API name of “Assign_suggested_user_ID_as_record_owner_ID”.

Under Set Field Values for the Contact Record, set OwnerId to {!Get_suggested_owner_user.Sales_Rep_User.Id}.

Save your Flow with the label “Contact Assign Sugg Sales Rep Flow (RTF)”.

Activate your Flow.

Validation
  1. Create a new Contact named “Tokyo Tester” and set their Mailing Country to Japan.
  2. Validate that it’s owned by Benita Borges.
  3. Create a new Contact named “Beijing Tester” and set their Mailing Country to China.
  4. Validate that it’s owned by Anush Amjit.

Harder way

You can’t call a subflow from a Record-Triggered Flow whose Optimize the Flow for is set to “Fast Field Updates,” which is a shame, because we’d get much better performance out of Salesforce if we could!

Luckily, Mitch Spano built a package called Trigger Actions Framework that lets us do what I call a “Record-Triggered(-ish) Flow”.

When we build Flows and Custom Metadata settings in Mitch’s style, we can effectively run Subflow from a Fast Field Update Flow!

A note on “-ish”:

Technically, you’re actually running an Apex “Before” Trigger that, thanks to Mitch’s package, checks the Custom Metadata settings to figure out which Autolaunched Flows to have Apex hand things off to.

Autolaunched Flows are allowed to run Subflow, so as long as you program your Autolaunched Flows to be compatible with Mitch’s package’s expectations (it’s picky about certain Flow-wide input/output variables), you get away with something Salesforce Record-Triggered Flow doesn’t natively allow.

That’s the “-ish” in “Record-Triggered(-ish) Flow”.

According to community members I trust, Mitch’s package runs just as fast as actual Record-Triggered Flows, if not faster, and definitely runs faster once you consider that “Before Triggers” / “Fast Field Update Flows” are known to run very quickly.

Work

If you’re up for a challenge, instead of creating a Record-Triggered Flow, create:

  1. An Autolaunched Flow called “Contact Assign Sugg Sales Rep Flow (TAF)” modeled off of “Account Assign Sugg Sales Rep Flow (TAF).” Remember to activate it – I always forget that step with Flow.
    • Tip: You’ll need to make sure this Flow includes a variable named “record,” of data type Record and object type Contact, with both Available for input and Available for output checked.
  2. A new sObject Trigger Setting Custom Metadata record labeled “TAF SOA Contact” based on the one labeled “TAF SOA Account,” but of course with everything that said “Account” saying “Contact” instead.
  3. A new Trigger Action Custom Metadata record labeled “TAF Contact Assign Sugg Sales Rep FlwBI” based on the one labeled “TAF Account Assign Sugg Sales Rep FlwBI,” but of course with everything that said “Account” saying “Contact” instead.
  4. A new Apex Trigger called “ContactTrigger_TAF” based on AccountTrigger_TAF,” but of course with everything that said “Account” saying “Contact” instead. Make sure it’s in Active status.
Validation
  1. Create a new Contact named “Tokyo Tester” and set their Mailing Country to Japan.
  2. Validate that it’s owned by Benita Borges.
  3. Create a new Contact named “Beijing Tester” and set their Mailing Country to China.
  4. Validate that it’s owned by Anush Amjit.
--- ---