Automate UiPath project documentation using PowerShell

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! :star_struck:

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.

  1. 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"

  2. 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.

  3. Inorder to render the arguments properly (with line breaks and unordered list) we perform some string manipulation

  4. 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("&lt;li&gt;","<li>").Replace("&lt;/li&gt;", "</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

  1. To consider the sequential order of the invoked workflows from Main.xaml (entry point)
  2. To read and document annotations in each of the sequences within a workflow
  3. 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
  4. Support variables and programatically get scope of each variable

Source code


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.

5 Likes

@jeevith

Excellent Jeevith, thank you very much for sharing. :slightly_smiling_face:

Thank you @StefanSchnell. My pleasure :slight_smile:

1 Like

@jeevith That’s excellent!! I shall try it right away with projects I am working with.

1 Like

Hi @JDoshi,

Sure. If your invoked workflows are of similar structure, you will not need to edit much in the code. Else, you might have to adapt the code accordingly to extract key/values.

If you have any challenges, post them here and I can try to help you out.

2 Likes

@jeevith , thanks for posting this! I am concerned that when the UiPath project code is delivered as compiled DLLs instead of as text xaml files, we’ll lose a lot of these tools from the community that have been developed over the years. I was wondering, are you thinking ahead to that time yet? If so, maybe you already have some ideas about that transition?

Hi @nikii,

Thank you for taking the time to discuss here.

I guess this is something I am not aware about.

Could you share a source where UiPath has wished to change how Studio files will be saved (dll instead of .xaml files)?
I would like to read up and understand this possible change, before I try to answer your question.

@jeevith , please disregard my question. I see that only the 64-bit and cross-platform published output (not the project source files) will be compiled (https://docs.uipath.com/studio/docs/about-automation-projects). Sorry for the confusion! Thanks again for the PS code.

Hi @nikii,

No worries. That is good to know as well :slight_smile:

For cross-platform projects one should be able to design workflows in .xaml format and later after publishing the project may be published file is in another format which can be executed in Mac and Linux OS as well as a background process.

Hi @jeevith,
A really nice piece of work. I gave it a try and works really fast. I have a couple of questions.

  1. In my case I run it on a local project. The file is generated successfully but I see some errors:
    image

  2. This is rather a feedback than the question, but it would be a really nice improvement if you could list variables and their scopes and imports that are part of each XAML.

Hi @Pablito,

Thank you for taking the time to test it. Your feedback is always welcome.

  1. $ArgumentsAnnotationText = $XAMLData.Activity.Members.Property."Annotation.AnnotationText" returns an empty array when the arguments have no annotations. Later when we try to go into the empty array (in your line 41) this error comes up, but since we are in a for loop, PowerShell gracefully handles the error and prints it to console without breaking from the loop and thats why the file is generated even when there are errors in value extractions.
    The solution here would be to have a try-catch for $ArgumentsAnnotationTextTemp = $ArgumentsAnnotationText[$i] in the catch we can specify an empty string (i.e., when an index error occurs, allocate an empty string). This will ensure that for those arguments where annotations are null, we catch and manipulate the $ArgumentsAnnotationTextTemp variable.

    Also if your local project has a different sequece, try-catch structure or nested sequences, the workflow annotations must be on the same level as shown in the sample project.

  2. I have a approach to extract them, it should be doable. The small challenge will be the extracting their scopes. I will updated this thread when I get it to work. Offcourse, the Github repo will be updated as well.

2 Likes

@jeevith,
Another small feedback :slight_smile:
I think you should promote this in the UiPath Marketplace.

1 Like