Target audience: CoE’s, RPA Solution Architects / Leads and RPA Developers
Technical Design Document (TDD) contains details of the design as implemented by the development team. In other words, this document is a detailed manual of the delivered solution and is quite an important resource for those who maintain software / robots in production.
The usefulness of this document is only realised when there is a knowledge gap in the team, which would mean that other developers need to quickly understand what the delivered code does and to get an quick overview of the workflows. This is even more critical when there is a need to hot fix bugs under production.
I have automated the tedious task of extracting details from workflows and tested this on a few projects in our organisation. Currently, we include the resulting document as one of the deliverables in the project.
Let’s begin!
In this tutorial, I will walk you through how you can automate the documentation of your UiPath project and use the output as an appendix to your TDD.
Scope
A script which can document the name of each workflow, the function of each workflow, the names of the arguments used, their data type and corresponding annotations. Variable name, type and annotations can also be captured, but for this tutorial, I will not be including them.
Why did I choose PowerShell?
- Is fast and reliable
- Requires zero dependencies
- Is already installed in windows environment and is available cross-platform
- Script can be run by anyone in the team to generate the required documentation
- PowerShell is awesome!
Script walkthrough
I will use the project zip file from my last tutorial Creating error-proof reusable workflows in UiPath as an example project and create the required documentation. The following script will download and unzip the zip file and make everything ready for the next stages.
function DownloadSampleFromUiPathForum{
# This function downloads, unzips files from the UiPath forum given the url of the file.
param (
[string]$SourceUrl
)
# Lets download a sample project file and unzip
$WebRequest = iwr $SourceUrl # Invoke-WebRequest to get headers
$WebRequest.Headers."Content-Disposition" -match -join("\w+.",$SourceUrl.Split(".")[-1])
$FileName = $Matches.0 # This gets us the filename from UiPath forum
$ZipFile = -join("C:\Users\", $env:UserName, "\Downloads\", $FileName)
$ExistsZipFile = Test-Path -Path $ZipFile
# Lets download only if the final is not found
if($ExistsZipFile -eq $false){
Invoke-WebRequest -Uri $SourceUrl -OutFile $ZipFile
Expand-Archive $ZipFile
}# if ends
}# function ends
# Calling the download function on the sample file
DownloadSampleFromUiPathForum 'https://forum.uipath.com/uploads/short-url/4j6bRIkvsRdRnsr7BhfcSHIwieF.zip'
The script which creates the documentation starts with first finding the current location and then defining the input $ProjectPath
. In this case, it is "C:\Users\"+$env:UserName+"\Downloads\TutorialRobustWorkflow\TutorialRobustWorkflow"
as this is where the DownloadSampleFromUiPathForum
function has downloaded and unzipped the project. You can also automate this part by probably converting this hardcoded value to a user input or programmatically identify, which folder to use.
# Find the current location
$CurrentDirectory = Get-Location | select -expand Path
# Provide UiPath project path - Not dynamic for tutorial purposes
$ProjectPath = -join("C:\Users\", $env:UserName, "\Downloads\TutorialRobustWorkflow\TutorialRobustWorkflow")
The output file is generated as a HTML file and needs to be declared and if it already exists the content within needs to be cleared. This way, the resulting html is always regenerated under runtime.
# Ensure the last folder level i.e., UiPath project name library name is used as the Output filename.
$OutputFile = $CurrentDirectory+"\"+$ProjectPath.Split("\")[-1].ToString()+".html"
# If output file exists, clear its content
$ExistsOutputFile = Test-Path -Path $OutputFile
if($ExistsOutputFile -eq $true){
Clear-Content $OutputFile
}
The .xaml files in a project can be located in any folder structure and this script will recursively search for .xaml files in each folder and the root folder of the UiPath project. In case you are trying this approach on REFramework, then I suggest you use the -Exclude
argument with the valid name of your entry point workflow. We call our entry point Initial.xaml in our REFramework templates.
In the sample project, we only have 2 files, Main.xaml
and RobustWorkflowTemplate.xaml
. However, you can have any number of .xaml files. The below script will populate the file objects in $FilesFound
variable. Since, we have to iterate to generate the data, lets also create an empty array $ReturnObj, which can store the return values for each workflow.
# Getting all .xaml files recursively in the $ProjectPath
$FilesFound = Get-ChildItem -Path $ProjectPath -Filter *.xaml -Recurse -File -Name # -Exclude "Initial.xaml" # Uncomment if using on REFramework
# Creating an empty array to save return objects
$ReturnObj = @()
The data is populated into the $ReturnObj
by iterating through the $FilesFound
and extracting data from .xml format.
Important to remember : each workflow you invoke should have a similar overall structure. For example, a try catch, a sequence within the try and so on. If each workflow has its own structure, this below code will fail. For example you Annotate a nested sequence instead of annotating the outermost sequence. Then $XAMLData.Activity.Sequence."Annotation.AnnotationText’’ will return the wrong annotation.
-
AnnotationText is found within the key
$XAMLData.Activity.Sequence."Annotation.AnnotationText"
It is important to remember that double quotes need to be used when polling for"Annotation.AnnotationText"
-
ArgumentsNames, ArgumentsType and ArgumentsAnnotationText are found in
$XAMLData.Activity.Members.Property.Name
,$XAMLData.Activity.Members.Property.Type
and$XAMLData.Activity.Members.Property."Annotation.AnnotationText"
respectively. Arguments for a workflow are always in global scope (accessisible in the entire workflow). On the contrary, variables can have varied scopes within a workflow. -
Inorder to render the arguments properly (with line breaks and unordered list) we perform some string manipulation
-
Since we have the returned object from each iteration of the for loop, we can populate them into the
$ReturnObj
variable. The following headers are extracted in this example :
# For each found file, extract the required values
ForEach($File in $FilesFound ){
$FilePath = $ProjectPath + '\' + $File
[xml]$XAMLData = Get-Content $FilePath -Encoding UTF8 # Read the .xaml file
# The try and catch needs to be in place as a precaution for variables not so important for arguments and annotations
try{$AnnotationText = $XAMLData.Activity.Sequence."Annotation.AnnotationText"
}catch{}
try{
$ArgumentsNames = $XAMLData.Activity.Members.Property.Name
$ArgumentsType = $XAMLData.Activity.Members.Property.Type
$ArgumentsAnnotationText = $XAMLData.Activity.Members.Property."Annotation.AnnotationText"
}catch{}
# Arguments as a single string value with corresponding type
$Arguments_Names = ''
$Arguments_Type = ''
$ArgumentsAnnotation_Text = ''
if($ArgumentsNames.Length -ne 0){
ForEach($i in 0..($ArgumentsNames.Count-1)){
$ArgumentsNamesTemp = $ArgumentsNames[$i]
$ArgumentsTypeTemp = $ArgumentsType[$i]
$ArgumentsAnnotationTextTemp = $ArgumentsAnnotationText[$i]
$Arguments_Names += "<li>"+ $ArgumentsNamesTemp + "`n" + "</li>"
$Arguments_Type += "<li>"+"$ArgumentsTypeTemp `n" + "</li>"
if($ArgumentsAnnotationTextTemp.Length -eq 0){
$ArgumentsAnnotation_Text += "None "
}else{$ArgumentsAnnotation_Text += "<li>"+"$ArgumentsAnnotationTextTemp `n"+"</li>"}
}
}# If ends
# Defining a custom PSObject
$obj = New-Object psobject -Property @{`
"File" = $File;
"WorkflowAnnotation" = $AnnotationText;
"ArgumentNames" = $Arguments_Names;
"ArgumentType" = $Arguments_Type;
"ArgumentsAnnotation" = $ArgumentsAnnotation_Text;
}
# These headers will be returned
$ReturnObj += $obj | select File,WorkflowAnnotation,ArgumentNames,ArgumentType,ArgumentsAnnotation
}# For loop ends
As an add-on, let’s also ensure the output is saved as a html file which makes it easy to share and customise with CSS, if required. To achieve this, there is a simple trick. First we prepare a string $HtmlHead which is populated by the <Style>
tag and provide all the CSS formatting within it. Later, PowerShell can add this CSS to the <head>
tag of the HTML output file.
# We would want to make the output a little nicer to read with some CSS
$HtmlHead = '<style>
body {
background-color: azure;
font-family: "Calibri";
}
table {
border-width: 1px;
border-style: solid;
border-color: black;
border-collapse: collapse;
width: 100%;
}
th {
border-width: 1px;
padding: 5px;
border-style: solid;
border-color: black;
background-color: #98C6F3;
}
td {
border-width: 1px;
padding: 5px;
border-style: solid;
border-color: black;
background-color: White;
}
tr {
text-align: left;
}
td:hover {
background-color: #ffff99;
}
tr:hover {
color: #0000FF;
font: bold Georgia, serif;
scale: 1.05;
}
overflow-wrap: break-word; /* Renamed property in CSS3 draft spec */
}
</style>'
The output from the earlier for loop can now be converted to HTML and the HTML file can also use the $HtmlHead
(custom CSS) in the -Head
argument.
# Converting and saving the PSObject as html and adding head to html output
$ReturnObj | ConvertTo-Html -Head $HtmlHead| Out-File $OutputFile
There is one more annoying thing when we parse our List items (argument names, type and annotation) and that is the <li>
tag is not correctly rendered in HTML. To avoid this failure, we can easily edit the character to the correct <li>
tag and pipe the result to the $OutputFile
.
# We have to replace the generated data and replace list formatting
$HTMLContent = Get-Content $OutputFile # Reading content
$HTMLContent.Replace("<li>","<li>").Replace("</li>", "</li>") | Out-File $OutputFile
Finally, we open up the HTML file in the default browser to show the overview of workflows.
# Opening the HTML file in the browser
Start-Process $OutputFile
This is how the output file looks like. That was it!
Scope for improvement
- To consider the sequential order of the invoked workflows from Main.xaml (entry point)
- To read and document annotations in each of the sequences within a workflow
- To apply 1 and 2 into REFramework, to create a document, which provides both the order and the annotations for the invoked workflows in Process.xaml
- Support variables and programatically get scope of each variable
Source code
-
The entire code can be found in github : DocumentationUiPathSampleProject.ps1
-
The resulting output for a sample project :
ProjectDocumentation_TutorialRobustWorkflow.html
I had great fun developing this. I wish you will try this and find it useful as well. If you wish to contribute, please send me a pull-request on github. Thank you for your attention.