Technical documentation

Hi everyone,
I need to create technical documentation for the processes.
Ideally, this documentation should be in .xlsx format and include the following for each workflow (starting from the main one):
+) packages used, along with their versions
+) activities used, including display names and property values
+) invoked workflows

Can anyone help me and explain how to do this?

Hello @simone.trenta,
Packages and their version you can read from the explorer panel under dependencies or just fetch from your project.json file

For the activities, you can copy them directly from your workflows or extract them from your workflow xaml file. In your xaml files you can find for example

<ui:ReadRange DisplayName="Read Excel" />
<ui:Click DisplayName="Login Button" />

Which shows activity type and displayname along with their properties.

For invoked workflows, you can also just copy from your actual workflows or extract from your xaml file. If extracting from xaml file, look for something like

<ui:InvokeWorkflowFile
    WorkflowFileName="Process.xaml" />

Br, Ave Jr

If you have access to copilot or something like that, then you can use that. I have had nice help from that and it would be easy to update.
It seems like a quite detailed documentation you are doing and will take a lot of time to keep up to date.

I think it would also be possible to create some kind of script to handle it, but I have not tried that.

@simone.trenta

Here is a PowerShell script generated by Claude and tested by me as it’s working.

<#
.SYNOPSIS
    Generates technical documentation (.xlsx) for a UiPath project by scanning every
    .xaml workflow in the project folder and all subfolders.

.DESCRIPTION
    For each workflow found, extracts:
      - Packages used (from project.json), with versions
      - Activities used, with display names and property values
      - Invoked workflows (InvokeWorkflowFile), with their argument bindings

    Produces a single .xlsx with 4 sheets: Workflows, Packages, Activities, Invoked Workflows.

.PARAMETER ProjectDir
    Root folder of the UiPath project. Defaults to the folder this script lives in,
    so the script can just be dropped into a project folder and run with no arguments.

.PARAMETER OutputPath
    Path of the .xlsx to generate. Defaults to "<ProjectDir>\Documentation\ProcessDocumentation.xlsx".

.PARAMETER ExcludeDirs
    Top-level or nested folder names to skip entirely (build/metadata folders, not process folders).

.EXAMPLE
    .\Generate-ProcessDocumentation.ps1
    Run from inside the project folder with no arguments.

.EXAMPLE
    .\Generate-ProcessDocumentation.ps1 -ProjectDir "C:\Projects\MyProcess" -OutputPath "C:\Reports\MyProcess.xlsx"
#>

param(
    [string]$ProjectDir = $PSScriptRoot,
    [string]$OutputPath = $(Join-Path $PSScriptRoot 'Documentation\ProcessDocumentation.xlsx'),
    [string[]]$ExcludeDirs = @('.local', '.settings', '.git', '.vs', 'bin', 'obj', 'node_modules', '.tmh')
)

$ErrorActionPreference = 'Stop'
$xNs = 'http://schemas.microsoft.com/winfx/2006/xaml'
$sap2010Ns = 'http://schemas.microsoft.com/netfx/2010/xaml/activities/presentation'

if (-not (Test-Path $ProjectDir)) { throw "ProjectDir not found: $ProjectDir" }
$ProjectDir = (Resolve-Path $ProjectDir).Path.TrimEnd('\')

Write-Host "Scanning project: $ProjectDir" -ForegroundColor Cyan

# ---------- 1. Packages (from project.json) ----------
$projectJsonPath = Join-Path $ProjectDir 'project.json'
$packages = @()
$mainEntry = 'Main.xaml'
if (Test-Path $projectJsonPath) {
    $projectJson = Get-Content $projectJsonPath -Raw | ConvertFrom-Json
    if ($projectJson.main) { $mainEntry = $projectJson.main }
    if ($projectJson.dependencies) {
        $packages = foreach ($dep in $projectJson.dependencies.PSObject.Properties) {
            [PSCustomObject]@{ Package = $dep.Name; Version = ($dep.Value -replace '[\[\]]', '') }
        }
    }
} else {
    Write-Warning "project.json not found under $ProjectDir - Packages sheet will be empty."
}

# ---------- 2. Discover every .xaml recursively, excluding build/metadata folders ----------
$allXaml = Get-ChildItem -Path $ProjectDir -Filter '*.xaml' -Recurse -File
$xamlFiles = $allXaml | Where-Object {
    $rel = $_.FullName.Substring($ProjectDir.Length + 1)
    $segments = $rel -split '[\\/]'
    -not ($segments | Where-Object { $ExcludeDirs -contains $_ })
}

function Get-Category([string]$relPath, [string]$mainEntry) {
    if ($relPath -eq $mainEntry -or (Split-Path $relPath -Leaf) -eq (Split-Path $mainEntry -Leaf) -and (Split-Path $relPath -Parent) -eq '') {
        return 'Main'
    }
    $parent = Split-Path $relPath -Parent
    if ([string]::IsNullOrEmpty($parent)) { return 'Root' }
    return ($parent -split '[\\/]')[0]
}

$fileList = foreach ($f in $xamlFiles) {
    $rel = $f.FullName.Substring($ProjectDir.Length + 1)
    [PSCustomObject]@{
        Item     = $f
        RelPath  = $rel
        Category = Get-Category $rel $mainEntry
    }
}
# Main first, then alphabetical by Category, then by file path
$orderedFiles = @($fileList | Where-Object { $_.Category -eq 'Main' }) +
                @($fileList | Where-Object { $_.Category -ne 'Main' } | Sort-Object Category, RelPath)

Write-Host "Found $($orderedFiles.Count) workflow file(s) after exclusions." -ForegroundColor Cyan

# ---------- helpers for activity/property extraction ----------
$excludeElementLocalNames = @(
    'Dictionary', 'List', 'Collection', 'ActivityAction', 'ActivityFunc',
    'DelegateInArgument', 'InArgument', 'OutArgument', 'InOutArgument',
    'Variable', 'Members', 'Property', 'Null', 'Reference', 'Boolean', 'String',
    'Int32', 'Double', 'Point', 'Size', 'PointCollection', 'Settings',
    'AssemblyReference'
)
$excludeAttrPrefixes = @('sap', 'sap2010', 'mc', 'xmlns')

function Truncate([string]$s, [int]$max = 300) {
    if ($null -eq $s) { return '' }
    $s = ($s -replace '\s+', ' ').Trim()
    if ($s.Length -gt $max) { return $s.Substring(0, $max) + '...' }
    return $s
}

function Get-PropertyElementSimpleValue($node) {
    $localName = $node.LocalName
    $propName = $localName.Substring($localName.LastIndexOf('.') + 1)
    if ($propName -in @('Arguments', 'Analytics', 'Output', 'ViewState')) { return $null }
    if ($node.Prefix -in @('sap', 'sap2010')) { return $null }
    $elemChildren = @($node.ChildNodes | Where-Object { $_.NodeType -eq [System.Xml.XmlNodeType]::Element })
    if ($elemChildren.Count -eq 0) {
        $t = Truncate $node.InnerText
        if ([string]::IsNullOrWhiteSpace($t)) { return $null }
        return "$propName=$t"
    }
    $wrapperTypes = @('InArgument', 'OutArgument', 'InOutArgument')
    $nonWrapperCount = @($elemChildren | Where-Object { $_.LocalName -notin $wrapperTypes }).Count
    if ($nonWrapperCount -eq 0) {
        $vals = $elemChildren | ForEach-Object { Truncate $_.InnerText } | Where-Object { $_ -ne '' }
        if ($vals.Count -eq 0) { return $null }
        return "$propName=" + ($vals -join '; ')
    }
    return $null
}

$activityRows = New-Object System.Collections.Generic.List[object]
$invokedRows = New-Object System.Collections.Generic.List[object]

function Walk-Node($node, $workflowName) {
    foreach ($child in $node.ChildNodes) {
        if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
        $localName = $child.LocalName
        $prefix = $child.Prefix
        $isPropertyElement = $localName -like '*.*'
        $isExcludedName = $excludeElementLocalNames -contains $localName

        if (-not $isPropertyElement -and -not $isExcludedName) {
            $displayName = $child.GetAttribute('DisplayName')
            $propParts = New-Object System.Collections.Generic.List[string]

            foreach ($attr in $child.Attributes) {
                $aPrefix = $attr.Prefix
                if ($aPrefix -in $excludeAttrPrefixes) { continue }
                if ($attr.LocalName -eq 'xmlns' -or $aPrefix -eq 'xmlns') { continue }
                if ($attr.LocalName -eq 'DisplayName') { continue }
                if ($attr.LocalName -in @('Name', 'Key', 'Class')) { continue }
                if ($aPrefix -eq 'x' -and $attr.LocalName -ne 'TypeArguments') { continue }
                $val = Truncate $attr.Value
                if ($val -eq '') { continue }
                $propParts.Add("$($attr.LocalName)=$val")
            }

            foreach ($pe in $child.ChildNodes) {
                if ($pe.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
                if ($pe.LocalName -notlike '*.*') { continue }
                $simple = Get-PropertyElementSimpleValue $pe
                if ($simple) { $propParts.Add($simple) }
            }

            $activityType = if ($prefix) { "$prefix`:$localName" } else { $localName }

            $activityRows.Add([PSCustomObject]@{
                    Workflow     = $workflowName
                    ActivityType = $activityType
                    DisplayName  = $displayName
                    Properties   = ($propParts -join ' | ')
                })

            if ($localName -eq 'InvokeWorkflowFile') {
                $wfFile = $child.GetAttribute('WorkflowFileName')
                $argParts = New-Object System.Collections.Generic.List[string]
                foreach ($sub in $child.ChildNodes) {
                    if ($sub.NodeType -eq [System.Xml.XmlNodeType]::Element -and $sub.LocalName -eq 'InvokeWorkflowFile.Arguments') {
                        foreach ($argNode in $sub.ChildNodes) {
                            if ($argNode.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
                            if ($argNode.LocalName -eq 'Dictionary') { continue }
                            $dir = $argNode.LocalName
                            $key = $argNode.GetAttribute('Key', $xNs)
                            $val = Truncate $argNode.InnerText 120
                            $argParts.Add("$dir $key = $val")
                        }
                    }
                }
                $invokedRows.Add([PSCustomObject]@{
                        Workflow        = $workflowName
                        DisplayName     = $displayName
                        InvokedWorkflow = $wfFile
                        Arguments       = ($argParts -join ' | ')
                    })
            }
        }

        Walk-Node $child $workflowName
    }
}

# ---------- 3. Parse every workflow ----------
$workflowSummary = New-Object System.Collections.Generic.List[object]

foreach ($f in $orderedFiles) {
    $fullPath = $f.Item.FullName
    try {
        [xml]$xml = Get-Content $fullPath -Raw
    } catch {
        Write-Warning "Skipping unparsable XAML: $($f.RelPath) - $($_.Exception.Message)"
        continue
    }
    $before = $activityRows.Count
    $beforeInv = $invokedRows.Count
    Walk-Node $xml.DocumentElement $f.RelPath

    $rootAnnotation = ''
    $rootEl = $xml.DocumentElement
    $ann = $rootEl.GetAttributeNode('Annotation.AnnotationText', $sap2010Ns)
    if (-not $ann) {
        foreach ($c in $rootEl.ChildNodes) {
            if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -notlike '*.*') {
                $a2 = $c.GetAttributeNode('Annotation.AnnotationText', $sap2010Ns)
                if ($a2) { $ann = $a2; break }
            }
        }
    }
    if ($ann) { $rootAnnotation = Truncate $ann.Value 400 }

    $workflowSummary.Add([PSCustomObject]@{
            File           = $f.RelPath
            Category       = $f.Category
            Description    = $rootAnnotation
            ActivityCount  = $activityRows.Count - $before
            InvokedWfCount = $invokedRows.Count - $beforeInv
        })
}

Write-Host "Packages: $($packages.Count) | Workflows: $($workflowSummary.Count) | Activities: $($activityRows.Count) | InvokedWorkflows: $($invokedRows.Count)" -ForegroundColor Cyan

# ---------- 4. Write the .xlsx ----------
$outDir = Split-Path $OutputPath -Parent
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }

if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
    Write-Host "ImportExcel module not found - attempting install for current user..." -ForegroundColor Yellow
    try {
        Install-Module ImportExcel -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
    } catch {
        Write-Warning "Could not install ImportExcel ($($_.Exception.Message)). Falling back to CSV export next to the requested output path."
    }
}

if (Get-Module -ListAvailable -Name ImportExcel) {
    Import-Module ImportExcel

    # If the target file exists but is locked (e.g. still open in Excel), fall back to a
    # timestamped filename instead of failing outright.
    if (Test-Path $OutputPath) {
        try {
            Remove-Item $OutputPath -Force -ErrorAction Stop
        } catch {
            $dir = Split-Path $OutputPath -Parent
            $base = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
            $ext = [System.IO.Path]::GetExtension($OutputPath)
            $stamped = Join-Path $dir "$base`_$(Get-Date -Format 'yyyyMMdd_HHmmss')$ext"
            Write-Warning "Could not overwrite '$OutputPath' (likely open in Excel). Writing to '$stamped' instead."
            $OutputPath = $stamped
        }
    }

    $workflowSummary | Export-Excel -Path $OutputPath -WorksheetName 'Workflows' -AutoSize -FreezeTopRow -BoldTopRow -AutoFilter
    $packages        | Export-Excel -Path $OutputPath -WorksheetName 'Packages' -AutoSize -FreezeTopRow -BoldTopRow -AutoFilter
    $activityRows    | Export-Excel -Path $OutputPath -WorksheetName 'Activities' -AutoSize -FreezeTopRow -BoldTopRow -AutoFilter
    $invokedRows     | Export-Excel -Path $OutputPath -WorksheetName 'Invoked Workflows' -AutoSize -FreezeTopRow -BoldTopRow -AutoFilter

    Write-Host "Documentation written to: $OutputPath" -ForegroundColor Green
} else {
    $base = [System.IO.Path]::ChangeExtension($OutputPath, $null).TrimEnd('.')
    $workflowSummary | Export-Csv "$base.Workflows.csv" -NoTypeInformation -Encoding UTF8
    $packages        | Export-Csv "$base.Packages.csv" -NoTypeInformation -Encoding UTF8
    $activityRows    | Export-Csv "$base.Activities.csv" -NoTypeInformation -Encoding UTF8
    $invokedRows     | Export-Csv "$base.InvokedWorkflows.csv" -NoTypeInformation -Encoding UTF8
    Write-Host "ImportExcel unavailable - wrote 4 CSV files next to: $base.*.csv" -ForegroundColor Yellow
}

This is the output:

Hi @

You can create an Excel documentation by reading the project’s XAML files and project.json.

Include:

  • Packages: Name + Version
  • Activities: Type + Display Name + Properties
  • Invoked Workflows: Workflow name + called workflow

For multiple workflows, start from the main workflow and follow the invoked workflows. This can also be automated using a small script/utility to extract the details into Excel.

I’d suggest starting with the main workflow and documenting each invoked workflow separately. You can capture the package names and versions, activity display names, key property values, and workflow dependencies in separate columns. If there’s a way to export this information automatically, that would save a lot of manual work, especially for larger projects.