Be sure to check out my Scripting4Crypto initiative. It’s a fun way to get into using cryptocurrencies all while getting your PowerShell needs met.
When working with file properties in ProjectWise, most administrators are accustomed to interacting with them through ProjectWise Explorer. But what if you need to retrieve those properties programmatically — specifically by Document GUID?
This post walks through a PowerShell-based approach that leverages native API calls from dmscli.dll to retrieve file properties for a document using its GUID.
This can be especially useful when:
- Building automation or reporting solutions
- Auditing document metadata
- Validating property values in bulk operations
- Troubleshooting property-related issues
Why Use the Document GUID?
The Document GUID provides a globally unique identifier for a document in the datasource. Unlike document numbers or names, it is immutable and safe to use in automation workflows.
If you already have the GUID — whether from audit trail records, integrations, or SDK calls — this method allows you to retrieve all associated file property values.
Approach Overview
Retrieving file properties requires three layers of data:
- Property Sets – Groupings of properties
- Property Definitions – Individual property metadata
- Property Values – Actual values assigned to the document
The workflow looks like this:
- Load required API methods from
dmscli.dll - Retrieve all Property Set metadata
- Retrieve all Property metadata
- Retrieve property values for the specific Document GUID
- Correlate everything into a structured DataTable
- Return the results
Step 1 – Load Native API Functions
We first load required methods from dmscli.dll using Add-Type with C# interop definitions:
aaApi_PropValueDataBufferSelectByDocaaApi_PropertyDataBufferSelectByRowIdaaApi_PropSetDataBufferSelectByRowId
These allow us to retrieve the underlying data buffers needed for property resolution.
Step 2 – The Get-PWFileProperties Function
Below is the full function implementation:
#region CREATE DMSFilePropertyFunctionstry {Add-Type -TypeDefinition @"using System;using System.Runtime.InteropServices;public class DMSFilePropertyFunctions{ /* HAADMSBUFFER aaApi_PropValueDataBufferSelectByDoc ( LPCGUID pDocGuid ) */ [DllImport("dmscli.dll", CharSet = CharSet.Unicode)] public static extern IntPtr aaApi_PropValueDataBufferSelectByDoc (Guid pDocGUID); /* HAADMSBUFFER aaApi_PropertyDataBufferSelectByRowId ( LONG propRowId ) */ [DllImport("dmscli.dll", CharSet = CharSet.Unicode)] public static extern IntPtr aaApi_PropertyDataBufferSelectByRowId (int propRowId); /* HAADMSBUFFER aaApi_PropSetDataBufferSelectByRowId ( LONG propSetRowId ) */ [DllImport("dmscli.dll", CharSet = CharSet.Unicode)] public static extern IntPtr aaApi_PropSetDataBufferSelectByRowId (int propSetRowId);}"@ -Language CSharp if ( [AppDomain]::CurrentDomain.GetAssemblies().GetTypes().Name -contains "DMSFilePropertyFunctions") { Write-Host "DMSFilePropertyFunctions is loaded." -ForegroundColor Green } else { throw "DMSFilePropertyFunctions is not loaded." }} catch { Write-Warning -Message $_}#endregion CREATE DMSFilePropertyFunctionsFUNCTION Get-PWFileProperties{ <# .SYNOPSIS Used to get the file properties. .DESCRIPTION Used to get the file properties for a document based on the document guid provided. .EXAMPLE # Gets the file properties from the provided document guid. $documentGUID = '00000a17-6474-4506-808e-913f46bc1c67' $results = Get-PWFileProperties -DocumentGUID $documentGUID -Verbose $results | ogv #> [CmdletBinding()] param( # Parameter description. [ValidateNotNullOrEmpty()] #[ValidateScript({ Get-PWFolders -FolderID $_ -JustOne })] [Parameter( HelpMessage = "Parameter description.", Mandatory = $true, Position = 0)] [string] $DocumentGUID ) #end param... BEGIN { $CmdletName = $MyInvocation.MyCommand.Name $StartTime = Get-Date Write-Verbose -Message "[BEGIN] $Start - Entering '$CmdletName' Function..." } #end BEGIN... PROCESS { try{ #region GET PROPERTY SET DATA $dtPropertySetData = [Data.Datatable]::new() $dtPropertySetData.Columns.Add("RowID", [int]) | Out-Null $dtPropertySetData.Columns.Add("Guid", [string]) | Out-Null $dtPropertySetData.Columns.Add("OrderNum", [int]) | Out-Null $dtPropertySetData.Columns.Add("DispName", [string]) | Out-Null $iPtrBuffer_PropertySetData = [dmsfilepropertyfunctions]::aaApi_PropSetDataBufferSelectByRowId(0) $iCount_PropSet = [pwwrapper]::aaApi_DmsDataBufferGetCount($iPtrBuffer_PropertySetData) for($index = 0; $index -lt $iCount_PropSet; $index++){ try{ $dr = $dtPropertySetData.NewRow() $dr.RowID = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertySetData, 1, $index) $dr.Guid = ([pwwrapper]::aaApi_DmsDataBufferGetGuidProperty($iPtrBuffer_PropertySetData, 2, $index)).ToString() $dr.OrderNum = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertySetData, 3, $index) $dr.DispName = [pwwrapper]::aaApi_DmsDataBufferGetStringProperty($iPtrBuffer_PropertySetData, 4, $index) $dtPropertySetData.Rows.Add($dr) | Out-Null } catch { Write-Warning -Message $_ } } # end for($index = 0; $index -lt $iCount_PropSet; $index++)... [pwwrapper]::aaApi_DmsDataBufferFree($iPtrBuffer_PropertySetData) Write-Verbose -Message "Successfully obtained Property Set data." #endregion GET PROPERTY SET DATA #region GET PROPERTY DATA $dtPropertyData = [Data.Datatable]::new() $dtPropertyData.Columns.Add("RowID", [int]) | Out-Null $dtPropertyData.Columns.Add("Guid", [string]) | Out-Null $dtPropertyData.Columns.Add("PropID", [string]) | Out-Null $dtPropertyData.Columns.Add("RowFlags", [int]) | Out-Null $dtPropertyData.Columns.Add("Type", [int]) | Out-Null $dtPropertyData.Columns.Add("OrderNum", [int]) | Out-Null $dtPropertyData.Columns.Add("DispName", [string]) | Out-Null $iPtrBuffer_PropertyData = [dmsfilepropertyfunctions]::aaApi_PropertyDataBufferSelectByRowId(0) $iCount_PropertyData = [pwwrapper]::aaApi_DmsDataBufferGetCount($iPtrBuffer_PropertyData) for($index = 0; $index -lt $iCount_PropertyData; $index++){ try{ $dr = $dtPropertyData.NewRow() $dr.RowID = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertyData, 1, $index) $dr.Guid = ([pwwrapper]::aaApi_DmsDataBufferGetGuidProperty($iPtrBuffer_PropertyData, 2, $index)).ToString() $dr.PropID = [pwwrapper]::aaApi_DmsDataBufferGetStringProperty($iPtrBuffer_PropertyData, 3, $index) $dr.RowFlags = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertyData, 4, $index) $dr.Type = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertyData, 5, $index) $dr.OrderNum = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer_PropertyData, 6, $index) $dr.DispName = [pwwrapper]::aaApi_DmsDataBufferGetStringProperty($iPtrBuffer_PropertyData, 7, $index) $dtPropertyData.Rows.Add($dr) | Out-Null } catch { Write-Warning -Message $_ } } # end for($index = 0; $index -lt $iCount_PropertyData; $index++)... [pwwrapper]::aaApi_DmsDataBufferFree($iPtrBuffer_PropertyData) Write-Verbose -Message "Successfully obtained Property data." #endregion GET PROPERTY DATA $iPtrBuffer = [dmsfilepropertyfunctions]::aaApi_PropValueDataBufferSelectByDoc($DocumentGUID) if($iPtrBuffer -eq [IntPtr]::Zero){ throw "Failed to get data buffer." } $iCount = [pwwrapper]::aaApi_DmsDataBufferGetCount($iPtrBuffer) #region CREATE DATATABLE $dtFileProperties = [Data.Datatable]::new() $dtFileProperties.Columns.Add("DocumentGuid", [string]) | Out-Null $dtFileProperties.Columns.Add("PropertySetDisplayName", [string]) | Out-Null $dtFileProperties.Columns.Add("PropertySetOrderNum", [int]) | Out-Null $dtFileProperties.Columns.Add("PropertyRowID", [int]) | Out-Null $dtFileProperties.Columns.Add("PropertyDisplayName", [string]) | Out-Null $dtFileProperties.Columns.Add("PropertyOrderNum", [int]) | Out-Null $dtFileProperties.Columns.Add("PropertyType", [int]) | Out-Null $dtFileProperties.Columns.Add("PropertyValue", [string]) | Out-Null #endregion CREATE DATATABLE for($index = 0; $index -lt $iCount; $index++){ # break $gDocGuid = [pwwrapper]::aaApi_DmsDataBufferGetGuidProperty($iPtrBuffer, 1, $index) $iPropRowID = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer, 2, $index) $iPropType = [pwwrapper]::aaApi_DmsDataBufferGetNumericProperty($iPtrBuffer, 3, $index) $sPropData = [pwwrapper]::aaApi_DmsDataBufferGetStringProperty($iPtrBuffer, 4, $index) $dr = $dtFileProperties.NewRow() $dr.DocumentGuid = $gDocGuid.ToString() $PropertyData = $dtPropertyData.Select("RowID = '$iPropRowID'") $propertySetType = $dtPropertySetData.Select("Guid LIKE '$($PropertyData[0].Guid)'") $dr.PropertySetDisplayName = $propertySetType[0].DispName $dr.PropertySetOrderNum = $propertySetType[0].OrderNum $dr.PropertyRowID = $iPropRowID $dr.PropertyDisplayName = $dtPropertyData.Select("RowID = '$iPropRowID'") | Select-Object -ExpandProperty DispName $OrderNum = $dtPropertyData.Select("RowID = '$iPropRowID'") | Select-Object -ExpandProperty OrderNum $dr.PropertyOrderNum = [int]$OrderNum $dr.PropertyType = $iPropType if($iPropType -eq 27){ $milliseconds = 3982041570000 $timespan = [TimeSpan]::FromMilliseconds($milliseconds / [TimeSpan]::TicksPerMillisecond) # Calculate total hours (including days) $totalHours = [math]::Floor($timespan.TotalHours) # Extract minutes, seconds, and fractional milliseconds $minutes = $timespan.Minutes $seconds = $timespan.Seconds $fractionalMilliseconds = $timespan.Milliseconds * 10000 # Format the result as HH:mm:ss.ffffff $editTime = "{0}:{1:D2}:{2:D2}.{3:D7}" -f $totalHours, $minutes, $seconds, $fractionalMilliseconds $dr.PropertyValue = "$editTime" } else { $dr.PropertyValue = $sPropData } $dtFileProperties.Rows.Add($dr) | Out-Null } # end for($index = 0; $index -lt $iCount; $index++)... [pwwrapper]::aaApi_DmsDataBufferFree($iPtrBuffer) } catch { Write-Warning -Message "[PROCESS] $_" } finally { if($dtFileProperties.Rows.Count -gt 0){ Write-Verbose -Message "Returning $($dtFileProperties.Rows.Count) rows of file properties data." Write-Output $dtFileProperties } else { Write-Verbose -Message "No data to return." } } } #end PROCESS... END{ $EndTime = Get-Date Write-Verbose -Message "[END] It took $($EndTime - $StartTime) to complete the process." Write-Verbose -Message "[END] $EndTime - Exiting '$CmdletName' Function..." } #end END...} #end FUNCTION Get-PWFileProperties...#Export-ModuleMember -Function Get-PWFileProperties
What the Function Does
1️⃣ Retrieves Property Set Data
We build a DataTable of:
- RowID
- GUID
- Display Name
- Order Number
This allows us to later determine which Property Set each property belongs to.
2️⃣ Retrieves Property Definitions
Another DataTable is built containing:
- Property RowID
- Property GUID
- Property Display Name
- Type
- Order Number
This provides the metadata needed to interpret property values.
3️⃣ Retrieves Property Values for the Document
Using:
aaApi_PropValueDataBufferSelectByDoc($DocumentGUID)
We retrieve all property values assigned to that document.
4️⃣ Correlates Everything
For each property value:
- Match Property RowID to Property metadata
- Match Property GUID to Property Set
- Construct a structured output row
The final output includes:
- Document GUID
- Property Set Name
- Property Name
- Property Type
- Property Value
Special Handling – Edit Time (Type 27)
Property Type 27 represents edit time in milliseconds.
The function converts this into a readable:
HH:mm:ss.fffffff
format for easier interpretation.
This is particularly helpful when auditing document activity.
Example Usage
$documentGUID = '00000a17-6474-4506-808e-913f46bc1c67'$results = Get-PWFileProperties -DocumentGUID $documentGUID -Verbose$results | Out-GridView
Verbose output will provide timing and processing details.
Final Thoughts
Working with file properties at the API level in ProjectWise can seem intimidating at first, but once the data buffer pattern is understood, it becomes very powerful.
Using the Document GUID ensures accuracy and reliability in automation workflows, and this function provides a clean, structured way to retrieve that data.
If you are building administrative tooling or reporting around file properties, this approach gives you full control over the metadata layer.
Experiment with it and have fun.
Hopefully, you find this useful. Please let me know if you have any questions or comments. If you like this post, please click the Like button at the bottom of the page. And thank you for checking out my blog.
