#ProjectWise, #PowerShell, #PowerWiseScripting, PWPS_DAB

HowTo: Retrieving File Properties by Document GUID in ProjectWise Using PowerShell

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:

  1. Property Sets – Groupings of properties
  2. Property Definitions – Individual property metadata
  3. Property Values – Actual values assigned to the document

The workflow looks like this:

  1. Load required API methods from dmscli.dll
  2. Retrieve all Property Set metadata
  3. Retrieve all Property metadata
  4. Retrieve property values for the specific Document GUID
  5. Correlate everything into a structured DataTable
  6. 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_PropValueDataBufferSelectByDoc
  • aaApi_PropertyDataBufferSelectByRowId
  • aaApi_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 DMSFilePropertyFunctions
try {
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 DMSFilePropertyFunctions
FUNCTION 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.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.