Usefulness5/5
Frequency: 5/5
Power: 2/5
Unintuitiveness: 2/5
Complexity: 1/5

Sub-Workflows

How (and why) to divide your project into smaller pieces

last changed at 2021-01-11 18:59:43 (UTC)
Beginner
Process Design
Splitting Things Up
UiPath

Splitting Things Up

This pattern is part of a series showcasing different techniques to split your business process into smaller pieces. Make sure to check out Splitting Things Up for an overview.

Situation

You get into the home office and start your computer. You start Studio and open your project. You open your workflow. But you know that loading Main.xaml in your project that deals with 20 different things takes at least 5 minutes: Great! Time for a coffee break!

Once Studio finally finished loading, you get to work. You have to fix a bug near the end of the workflow, but doing so requires some experimentation. Which means you have to execute the whole thing until that point… which takes another 5 minutes every time you change any little thing. I hope you won’t get a caffeine overdose!

Admittedly, this situation is slightly exaggerated, but projects with a single workflow file containing thousands of activities have been spotted in the wild.

While I don’t like the term best practice much, terrible practices like this are often much easier to spot.

Compare

The pattern Extract as You Go provides a hands-on example of how to leverage sub-workflows for quickly developing new projects.

Common examples

  • To move logical parts of your workflow into their own files (separation of concerns)
  • It’s getting hard to find the part of the workflow you’re looking for (readability)
  • Too many nested levels of container activities (readability)
  • Testing the whole process from scratch for every change slows you down (testability)
  • Adding test cases to create unit tests for parts of your process (testability)
  • You received a messy workflow created by a citizen developer in StudioX and need to refactor it (readability)

Solution: Sub-workflows

Sub-workflows are additional .xaml files that can be invoked (i.e. executed) from other workflows through the Invoke Workflow File activity. This allows splitting up your process into multiple parts to streamline development and make things easier to read.

How to use

  1. Move some activities (that you want to factor out) into a container — typically a Sequence
  2. Right-click the container and choose Extract as Workflow
  3. Verify that all arguments have been recognized correctly

Alternatively:

  1. Create a new workflow from the Ribbon
  2. Manually add workflow arguments or copy&paste them from a different workflow
  3. Drag&drop workflow files from the Project panel into the workflow to create an Invoke Workflow File activity
Extract as Workflow option in the context menu
Extract as Workflow option in the context menu

Case study

One of the best ways to learn this sort of implicit knowledge is by example, so for the Splitting Things Up series, we have a simple master data entry workflow we will modify as needed for the different Patterns. Please refer to the main guide for the initial situation. We will start where the parent guide left off.

Please make sure to clone the github repository to play along, which is much more fun and will give you “muscle memory” you just cannot get from passively reading something. You can do the steps in any order, but I like to go inside-out because that ensures I don’t forget anything important.

Below is an overview of the initial state after we fixed a small bug. If you remember, this was a pretty painful fix, so we vowed not to ever go through that again.

Furthermore, as you can see, the two different data entry tasks, supplier creation and supplier change, share a lot of common code. Ideally, we should be able to reuse this code rather than having two copies (so in case any maintenance has to be done, we only have to do it in one place). What can we do to improve the situation?

Process before refactoring
Process before refactoring

Supplier change outsourcing

The first issue we’re going to tackle is the code redundancy. As the supplier change is fully duplicated into the supplier creation, we will extract that first. I dragged out the Log Message and created a new sequence because I want to reuse the extracted workflow.

Re-arranged supplier change to make extraction easier
Re-arranged supplier change to make extraction easier

Next, we will seize the opportunity to make things more testable by introducing variables for all the column values in the current row. This will allow us to test the supplier change separately without having to worry about creating a DataTable — simply by setting default values, or by creating test cases.Test cases are definitely preferable if you have Studio Pro.

One word of caution, however: make sure you remove all default values once you go into integration or user acceptance testing. Since UiPath uses the default value when a workflow argument is left empty during invocation, leaving default values in place can lead to nasty, hard-to-spot bugs.

It should look roughly like this (expand to see solution):

Supplier change before extraction
Supplier change before extraction

You can only really see what I did with the TypeInto for InternalName, but it’s the same logic for all of the fields.

In case you’re wondering why I don’t use Multiple Assign, the regular Assign is easier to copy&paste and provides more meaningful error messages. So I still slightly prefer it even though it takes up more screen space.

Now we’re ready to hit that juicy Extract as Workflow button. Unfortunately, Studio is not yet smart enough to realize that we also need to copy the Use Application/Browser scope, so we have to do it manually.

My recommendation is to use a UI Descriptor to reuse the same selector and URL across the whole project. If you don’t feel comfortable with the Object Repository yet or want to use the classic Attach Browser, here is an alternative to quickly re-wrap the extracted workflow while copying the selector, etc.

  1. Open the sub-workflow
  2. cut the sequence
  3. paste it to a text editor (or, recommended, use something like Ditto)
  4. copy the Use Browser
  5. delete the content
  6. copy&paste back the content from step 2.

As always, make sure to check your arguments. In my case, the industry argument went missing because (at the time of writing) Studio does not recognize variables as necessary for the sub-workflow if we only use them in a selector. To fix it, copy&paste the variable from Main and turn it into an Argument.

No industry?
No industry?

You should now be left with something like this in Main, which is much more readable:

Supplier change in Main
Supplier change in Main

Supplier creation shenanigans

We can reuse much of the work we did for the supplier change in the supplier creation. The first step is to change the scope of the variables we just created to put them into the body of the for loop. This will make it possible to also use them for the supplier creation.

Could we have just created them in the loop body in the first place? Yes, but I felt that explaining it this way is easier to understand and gives me an opportunity to show you a little trick:

Changing the variable scope can be done most easily by selecting all the variables you want, copying, deleting them, then pasting them into the other scope — which only takes a few seconds. Just make sure to delete industry in the supplier create branch, too.

After this operation, we should also create variables for the additional values needed to create new suppliers. Now we can delete the activities that are associated with the supplier change and replace them with a call to CreateSupplier.

Last, we also have to move the rest of the clicks into a sequence and extract the whole thing as a workflow. I usually do that by zooming out and using a box select, but feel free to do it any other way if you prefer. After extracting, you need to duplicate the Use Application/Browser again.

Once done, your workflow should look something like below:

Workflow after first round of refactoring
Workflow after first round of refactoring

Much more readable, especially if you collapse the relatively uninteresting AssignVariables sequence. Since the SupplierChange workflow is now invoked in both cases, I dragged it out of the If.

More importantly, we can now test the SupplierChange and SupplierCreate workflow independently. Go ahead and give it a try by opening the New Supplier form in the demo app manually, entering some default values for the arguments, and executing ChangeSupplier.

But what’s this? It doesn’t seem to work quite right… Oh! We forgot that the sequence also contained a click on the edit button — this has to be in Main instead. I also inadvertently removed the click on New Supplier for the left branch. Let’s fix that.In case you’re wondering, no, I didn’t make these mistakes on purpose, but I think they’re instructive, so I left the section in.

Main after fixing our two bugs
Main after fixing our two bugs

Extracting the body

This is already much nicer in terms of our ability to test changes, but we still have to run through all rows for debugging, i.e. we cannot create/change a supplier without our Excel file. Having that ability would be great for testing.

So let’s rearrange some more: create a new sequence inside the For Each to contain everything after AssignVariables, then use Extract as Workflow on it. Easy-peasy.

We’re really getting somewhere now in terms of the single responsibility principle, too: each of our workflows does one specific thing now rather than having a single big ol’ mess of “run the supplier process”.

Workflow after extracting the loop body
Workflow after extracting the loop body

Separating data input and entry

To further further the goal of doing only one thing,Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo let’s also create a workflow that handles our data entry.

First, move the For Each Row out of the Excel Application Scope (don’t forget changing the variable scope of the DataTable to Main). Then, enclose the latter with a sequence, drag it out of the Use Application and extract it. Make sure to check the argument is correctly recognized as Out.

Do the same thing for the data entry part with the Use Application and For Each Row. We are left with a very simple Main workflow:

Final worflow
Final worflow

This is a good time to run an end-to-end test. You might find, as I did, that the date assignment doesn’t work if the column is empty, so it fails on the second row. Most variable types are fine with empty columns, but not DateTime, so I changed it to this

if(not IsDBNull(row("Date")), Convert.ToDateTime(row("Date")).ToString("MM/dd/yyyy"), Nothing)

After all that talk about being able to test things in isolation, you may have gotten the impression that those are the only tests needed. If so, you are mistaken: it still makes sense to test everything end-to-end from time to time, especially with variable input data — but now that is an option, not a necessity.

Final result

If you want to compare your workflow to my reference solution, you can find the final version here.

Code Quality

Will Maggie care about the fruits of our labor? Probably not. Since business users typically don’t notice any difference when executing the workflow,Maggie is our internal customer from procurement. I know, it’s easy to forget we’re not just doing this for fun it is unfortunately very alluring for developers to cut corners and go for the quick&dirty route — maintenance be damned.

The problem is that, if you find any more bugs, or have to perform maintenance in the future, Maggie will become very disenchanted with the ever-increasing delays. Code quality is one of those things you don’t notice when they are there, but you do start to notice them very emphatically when they go missing.

Unfortunately, my experience looking at customers’ workflows is that, more often than not, either corners were cut or the developers did not know that they were getting deeply into technical debt. This is true for both first-party and third-party developers. Make sure you keep code quality at a reasonable level, or you will end up in maintenance hell.Anecdotally, third-party developers are worse about this — which is understandable given that their incentives bias them towards delivering something that barely works and requires their renewed services to fix, rather than something that is easily maintainable.

But I wouldn’t be me if I didn’t also caution you about going too far in the other direction: when deciding whether or not to expend effort on improving code quality, you should always take into consideration variables like how business critical the process is, the probability of process or target application changes, whether it is good enough as it is, the costs and opportunity costs caused by the additional work, etc.

If you apply unnecessarily high standards, you sacrifice a lot of agility and ability to execute for no gain. In particular, I don’t think the first version of a process should be very polished: put it to the test with your business users first and chances are it will be unrecognizable after making all their required changes anyways.

When to use

  • Testability: you find yourself repeatedly running the workflow from the start for minor changes
  • Reusability: reduce code duplication by factoring out common automation paths
  • Readability: you spend a lot of time navigating your workflow rather than automating
  • Better error messages: it’s getting hard to find the activity that caused an error

Comments

  • I used the modern design experience from Studio 20.10, so you might need to update if you want to load the example.
  • Splitting up your workflow is usally a good idea, but make sure you don’t split it too early — i.e. before you know which data is needed for a sub-workflow. Adding arguments later takes a lot more time than using Extract.
  • Also make sure you don’t split too much. The single responsibility principle is a useful rule of thumb, but in contrast to normal coding, in UiPath there is a trade-off between the advantages of having a clear responsibility versus readability and navigation overhead.Because every “function” is in its own file and you cannot see the arguments directly in the parent workflow
  • Workflow analyzer has a rule to prefix arguments with in_ and out_, which is officially considered a best practice by UiPath. I disagree.
  • Keep in mind opportunity costs and other factors when deciding whether invest in code quality improvements. In the end, this is a play for better maintainability at the expense of speed.

Exercises

  1. Download the workflow from here and perform the different steps yourself if you haven’t done so already.
  2. What are the advantages of using Extract as Workflow when compared to creating sub-workflows manually?
  3. Why did we create variables for all the entries in each row rather than simply passing row as an argument?
  4. When would it make sense to create a sub-workflow from scratch rather than extracting it from a larger existing workflow?

Solutions

  1. Arguments are created automatically, which yields a major productivity boost.
  • Variables make it explicit on the “surface” which data is needed in the sub-workflow. Just using row requires always having to check the implementation
  • It enables setting test values (default values or test cases), which is harder to do for DataRow arguments
  • It allows us to fail early during the assign variable state, before entering any data — not that important here, but in many real-world use cases this lets us avoid having to roll back a partially created data entry
  • It makes business rules for the different columns easier to implement and understand
  • DataRow is not serializable, so we cannot pass it across a process boundary (this will become important later in the series)
  • You are working bottom-up and want to create parts to assemble later
  • You are working top-down and want to specify parts before implementing them
  • You don’t know exactly what you want to do yet and creating the sub-workflow helps you reason about it
  • You need a recursive algorithm of any kind (e.g. tree traversal)
  • To create utility functions
  • You can envision a more generic way to solve your problem than the current implementation allows

References


© 2021, Stefan Reutter