Reporting Objects
Eventually, a script extracts all the required data and it’s time to generate some form of report. Some of the common ways to make the output generated by a script available to people include:
These actions mark the final stage of a script. Before a script can post, send, or store anything, the data retrieved from objects must be held in a PowerShell object, usually an array or a list.
I use the same approach to record data in all scripts. First, I create a PowerShell list:
$Report = [System.Collections.Generic.List[Object]]::new()
Then, after the script processes an object, the data to capture is handled by populating the properties of a custom object and writing it to the list. In performance terms, this approach is better than adding items to an array because PowerShell recreates the array for each addition. That’s OK for a couple of items but not so good when dealing with thousands. Updating the list is simple. For instance:
$ReportLine = [PSCustomObject]@{ TimeStamp = $M.Received Sender = $M.SenderAddress Recipient = $M.RecipientAddress Subject = $M.Subject Status = $M.Status Direction = $Direction SenderDomain = $SenderDomain RecipientDomain = $RecipientDomain }
$Report.Add($ReportLine)The output list is easy to work with and easy to transform into other forms. For instance, to include the data in a HTML file, the script can convert the list like this:
$HTMLData = $Report | ConvertTo-HTML -Fragment
Filtering and Limits
[array]$Mailboxes = Get-ExoMailbox -Filter {Office -eq "Dublin"} -RecipientTypeDetails UserMailbox -ResultSize Unlimited[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member' and OfficeLocation eq 'Dublin'" -ConsistencyLevel eventual -CountVariable Records -All
Nevertheless, the point remains that the golden rule is to only fetch the data you need to process. Using good filters is the best way to accomplish this goal.
Lots More to Learn about Microsoft 365 PowerShell
Hopefully, you’ll find that the many Practical365.com articles we publish about using PowerShell to get real work done help. And if you want to browse code, feel free to look through my GitHub repository. You might even find some working code there.
Interpreting Objects
Once you’ve found some objects, the next step is to make sense of the data. Most PowerShell cmdlets return easy-to-use properties, but there are a few things to watch out for with some Microsoft 365 cmdlets.
In some cases, the value returned for a property is an array and some work is needed to get the full information that you might want. Consider this example of finding who manages a distribution list:
Get-DistributionGroup -Identity VIPusers | Format-List ManagedBy
ManagedBy : {tony.redmond, Lotte.Vetler, d36b323a-32c3-4ca5-a4a5-2f7b4fbef31c}The ManagedBy property holds the name of each owner account. To get more information about the owners, use the names with another cmdlet like Get-ExoMailbox. For example:
[array]$ManagedBy = Get-DistributionGroup -Identity VIPUsers | Select-Object -ExpandProperty ManagedBy $ManagedBy | Get-ExoMailbox | Format-Table DisplayName, Alias, Name DisplayName Alias Name ----------- ----- ---- Tony Redmond Tony.Redmond tony.redmond Lotte Vetler Lotte.Vetler Lotte.Vetler Kim Akers (She/Her) Kim.Akers d36b323a-32c3-4ca5-a4a5-2f7b4fbef31c
Some of the Microsoft Graph PowerShell SDK cmdlets obscure valuable information. For example, the Get-MgGroupMember cmdlet returns a list of account identifiers for group members:
Get-MgGroupMember -GroupId (Get-MgGroup -Filter "Displayname eq 'System Innovation'").Id Id DeletedDateTime -- --------------- eff4cd58-1bb8-4899-94de-795f656b4a18 a3eeaea5-409f-4b89-b039-1bb68276e97d 67105a51-e817-493e-8094-f600babf5f62 d36b323a-32c3-4ca5-a4a5-2f7b4fbef31c
If you want details of the group members, a different approach is necessary to retrieve the information from the additionalProperties property:
$GroupId = (Get-MgGroup -Filter "displayName eq 'System Innovation'").Id
Get-MgGroupMember -GroupId $GroupId | Select-Object -ExpandProperty additionalProperties
Key Value
--- -----
@odata.type #microsoft.graph.user
businessPhones {+1 713 633-5141}
displayName Kim Akers (She/Her)
givenName Kim
jobTitle VP Marketing
mail Kim.Akers@office365itpros.com
mobilePhone +1 761 504-0011
officeLocation NYC
preferredLanguage en-US
surname Akers
userPrincipalName Kim.Akers@office365itpros.comBefore you ask, it’s the Graph that returns data in this way.
The unified audit log is a great source of valuable information about who did what and when inside a Microsoft 365 tenant. Workloads capture audit events as people interact with applications and those events eventually end up in the audit log. The Search-UnifiedAuditLog cmdlet (which has had some recent problems) finds data from the log. A lot of interesting detail is contained in a JSON-format property called AuditData. Before you can extract information from that property, you must convert it from JSON.
$Records = search-UnifiedAuditLog -StartDate 18-Oct-2023 -EndDate 19-Oct-2023 -Operations FileModified -Formatted -ResultSize 5000 -SessionCommand ReturnLargeSet
After finding the right event, we can convert its AuditData property:
$AuditData = $Records[0].AuditData | ConvertFrom-Json
And then all the fields in the property are available for use in a script. For instance, here’s the site URL for the modified document:
$AuditData.SiteUrl https://office365itpros.sharepoint.com/sites/BlogsAndProjects/
At this point, you might wonder how to deal with all of the edge cases you might encounter. Trial and error and good internet searches are the normal response. Someone has probably met and overcome a problem you meet, and maybe there’s a Practical365.com article that helps. Just be persistent.

Getting Objects
All the Microsoft 365 modules are well-equipped with cmdlets to fetch objects. Sometimes multiple cmdlets exist that can fetch the same objects, meaning that the trick is to make the right choice of which cmdlet to use. That decision depends on the context. We’ll get to that later.
Table 1 lists some of the basic objects used in Microsoft 365 and the cmdlets available to fetch these objects.
| Object type | Cmdlets | Module |
| User accounts | Get-MgUser | Microsoft Graph PowerShell SDK |
| Mailboxes | Get-Mailbox and Get-ExoMailbox | Exchange Online management |
| SharePoint Online sites | Get-SPOSite Get-MgSite | SharePoint Online management Microsoft Graph PowerShell SDK |
| OneDrive for Business accounts | Get-SPOSite Get-MgSite | SharePoint Online management Microsoft Graph PowerShell SDK |
| Microsoft 365 Groups | Get-UnifiedGroup Get-MgGroup | Exchange Online management Microsoft Graph PowerShell SDK |
| Teams | Get-Team Get-MgTeam | Microsoft Teams Microsoft Graph PowerShell SDK |
| Devices | Get-MgDevice | Microsoft Graph PowerShell SDK |
Do Three Fundamentals of Microsoft 365 PowerShell Well and Scripting is Much Easier
As some of you might know, I write a lot of PowerShell. I’m not a very good PowerShell developer. Most of my code ignores fundamentals like error handling because I write scripts to explore and explain principles of getting work done rather than to develop complete solutions. In any case, that’s my excuse.
- Get objects.
- Interpret objects.
- Report objects.




