Create your own
Lesson illustration

Filtering, Selecting, Sorting, and Grouping PowerShell Objects

Hello. In the previous lesson, you processed one identity record at a time with foreach or ForEach-Object, preserving structured output as PSCustomObject instances. That skill lets you apply a decision consistently across a population.

Now we will shape that population into useful IAM reports. You will learn to retain only relevant objects, choose the properties a reviewer needs, put records in a meaningful order, and summarize patterns such as enabled accounts by department. The key rule remains: until you deliberately format at the end, the pipeline carries objects, not display text.


Four ways to shape an identity-object stream

Consider a collection of access decisions produced by the prior lesson. This is in-memory sample data only; no directory is queried or changed.

$accessDecisions = @(
    [pscustomobject]@{
        UserPrincipalName = "maya.chen@contoso.com"
        Department        = "Finance"
        AccountState      = "Enabled"
        Decision          = "EligibleForProvisioning"
    }

    [pscustomobject]@{
        UserPrincipalName = "omar.hassan@contoso.com"
        Department        = "Finance"
        AccountState      = "Suspended"
        Decision          = "DoNotProvision"
    }

    [pscustomobject]@{
        UserPrincipalName = "riley.smith@contoso.com"
        Department        = "Sales"
        AccountState      = "Enabled"
        Decision          = "ManualReview"
    }

    [pscustomobject]@{
        UserPrincipalName = "taylor.woods@contoso.com"
        Department        = "Finance"
        AccountState      = "Enabled"
        Decision          = "NoChange"
    }

    [pscustomobject]@{
        UserPrincipalName = "nora.patel@contoso.com"
        Department        = "HR"
        AccountState      = "Enabled"
        Decision          = "ManualReview"
    }

    [pscustomobject]@{
        UserPrincipalName = "li.wong@contoso.com"
        Department        = "Finance"
        AccountState      = "Enabled"
        Decision          = "EligibleForProvisioning"
    }
)

The four cmdlets in this lesson make distinct changes to an object stream:

CmdletPrimary questionEffect on pipeline objects
Where-ObjectWhich records qualify?Removes objects that do not meet a condition.
Select-ObjectWhich properties should this report expose?Projects selected or calculated properties.
Sort-ObjectIn what order should records appear?Reorders objects.
Group-ObjectHow many records share a property value?Replaces individual objects with group-summary objects.

The order is significant. Filtering before selecting is often necessary because the filter needs properties that might otherwise be removed. Grouping is usually a reporting endpoint because it changes the output from individual identity records into summaries.

This diagram shows PowerShell process objects being filtered by the `CPU` property and then sorted. The same pipeline pattern applies to identity objects: each stage receives structured objects, evaluates their properties, and passes structured results to the next stage.

Unleash the Power of PowerShell Pipelines for Beginners!

Watch “Unleash the Power of PowerShell Pipelines for Beginners!” by Travis Roberts for a compact visual demonstration of sorting, filtering, and combining pipeline stages.

Watch sorting to see Sort-Object arrange process objects by a property. Then watch filtering for the mechanics of Where-Object and $_, followed by combined stages to see a filter followed by a sort. Translate the process examples mentally into user or access-decision records.


Filter objects with Where-Object

Where-Object tests each incoming object. If its script block evaluates to $true, the object continues through the pipeline; otherwise, it is removed.

For example, a reviewer may first want to see only enabled accounts:

$enabledUsers = $accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled"
    }

Within the script block, $_ is the current object. On one pass, it represents Maya’s decision object; on another, Riley’s. Where-Object does not turn those records into strings or construct a simplified report. It passes the matching original objects onward, complete with their properties.

The prior lesson covered comparison and logical operators, so filters can express policies with multiple conditions:

$priorityCases = $accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled" -and
        $_.Decision -in @(
            "EligibleForProvisioning",
            "ManualReview"
        )
    }

This produces enabled users whose decisions need either an access action or human review. It excludes suspended accounts and users already classified as NoChange.

Filter early, and filter at the best available layer

When the data comes from an external system, reduce unnecessary retrieval at the source when that system supports it. For example, directory and API commands often offer server-side filtering parameters. If source-side filtering is unavailable or the condition depends on values you calculate locally, use Where-Object as early as practical.

This matters in identity work. Pulling thousands of directory objects only to retain a dozen for a report increases latency, load, and the amount of sensitive data handled by the script.

One-liners and the pipeline - PowerShell 101 - Microsoft Learn

Read the “Filter Left” and “Command sequencing for effective filtering” portions of Microsoft Learn’s PowerShell 101. They establish why filtering at the earliest viable point improves efficiency and why pipeline order affects correctness.

In the “Filter Left” section, read from the filtering principle through the Get-Service examples. Focus on the distinction between asking the source for a narrow result and retrieving everything before filtering locally. Then, in “Command sequencing for effective filtering,” read the ordering explanation and compare the failing Select-Object-then-Where-Object command with the corrected order.

A useful diagnostic habit is to inspect the result at an intermediate stage:

$enabledUsers |
    Select-Object -Property UserPrincipalName, Department, Decision

Do this while developing a report, before adding a final formatter. You can confirm that the expected records survived the filter and that their data is still structured.


Select report properties without losing needed data too soon

Directory objects often contain dozens of properties. A reviewer usually needs a smaller, intentional view. Select-Object projects the properties you name:

$enabledUsers |
    Select-Object -Property UserPrincipalName, Department, Decision

The output now has three report-oriented properties per record. This is a good place to distinguish selection from filtering:

  • Where-Object answers whether an entire record remains.
  • Select-Object answers which properties of each remaining record are present in the output.

Suppose you place selection before a later filter:

$accessDecisions |
    Select-Object -Property UserPrincipalName, Department, Decision |
    Where-Object {
        $_.AccountState -eq "Enabled"
    }

This fails conceptually because AccountState is no longer part of the selected output. The condition cannot inspect a property that was discarded. Filter on AccountState first, then select the report fields:

$accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled"
    } |
    Select-Object -Property UserPrincipalName, Department, Decision

This does not mean Select-Object must always be near the end. It means every downstream command must receive the properties it needs. In a longer automation pipeline, keep the shape of the current objects in mind just as carefully as you track variable types in a program.

You can also create a calculated property. The following report renames AccountState to State without modifying the original objects:

$accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled"
    } |
    Select-Object -Property `
        UserPrincipalName,
        Department,
        @{Name = "State"; Expression = { $_.AccountState }},
        Decision

The hash table inside @{ ... } defines the calculated property:

  • Name becomes the column or property name in the output.
  • Expression is a script block evaluated for each incoming object.

Here the calculation merely renames a field, but later it can derive values such as a normalized identifier or an access-review status. Keep such transformations explicit and reviewable.

Before selecting properties from an unfamiliar command, inspect actual objects rather than relying on what the default console view happens to show.

Discovering objects, properties, and methods - PowerShell 101

Read the Microsoft Learn discussion of Get-Member and Select-Object. It reinforces the object-first workflow needed when identity cmdlets expose more attributes than their default display reveals.

In the “Get-Member” and “Properties” sections, read the object-inspection introduction, then continue through the Get-Service examples that use Select-Object -Property. Focus on the fact that a default display is not a complete schema. Next, read the Active Directory user objects example in the later “Properties” section, beginning with the explanation that Get-ADUser returns a limited default set, and note the use of -Properties to request only required directory attributes.

For a live directory query, requesting only needed attributes is separate from selecting properties for the final report. The first limits what is retrieved; the second shapes the local pipeline output. Both are useful, but they happen at different points.


Sort records by meaningful properties

Sort-Object orders objects by one or more properties. By default, it sorts ascending:

$enabledUsers |
    Sort-Object -Property Department, UserPrincipalName |
    Select-Object -Property UserPrincipalName, Department, Decision

This sorts first by Department. Within the same department, it sorts by UserPrincipalName. Multi-property sorting is especially useful for access-review queues: sort by a business grouping, then by a stable identity identifier.

For a descending order, use -Descending:

$accessDecisions |
    Sort-Object -Property UserPrincipalName -Descending |
    Select-Object -Property UserPrincipalName, AccountState, Decision

Sorting changes order, not membership. The same objects remain in the pipeline.

Be attentive to property types. Numeric counts and actual [datetime] values sort in their natural numerical or chronological order. But values stored as loosely formatted strings can sort unexpectedly. For example, strings such as "2/1/2025" and "11/15/2024" are not a reliable basis for chronological sorting across formats. When designing identity data, preserve dates as date values and counts as numbers whenever possible.

A safe, reviewer-friendly pipeline might first filter enabled action cases, then sort them, then project the columns needed for approval:

$accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled" -and
        $_.Decision -ne "NoChange"
    } |
    Sort-Object -Property Department, UserPrincipalName |
    Select-Object -Property UserPrincipalName, Department, Decision |
    Format-Table -AutoSize

Format-Table belongs only at the display boundary. It turns results into formatting instructions intended for the console, so do not place it before commands that still need object properties.


Group records to create summaries and retain drill-down capability

Filtering and sorting work with one object per person. Group-Object is different: it summarizes objects that share the same value for a property.

To count enabled identity records by department:

$departmentGroups = $enabledUsers |
    Group-Object -Property Department

$departmentGroups no longer holds user records directly. It holds group objects. Each group object includes:

Group propertyMeaning
NameThe grouping value, such as Finance.
CountNumber of input objects in that group.
GroupThe actual collection of original objects belonging to that group.

Create a concise department summary by selecting the group key and count, then sorting the counts from largest to smallest:

$departmentGroups |
    Sort-Object -Property Count -Descending |
    Select-Object -Property Name, Count |
    Format-Table -AutoSize

With the sample data, Finance has three enabled users, while Sales and HR each have one.

Crucially, the detailed records are still available through the Group property:

$financeGroup = $departmentGroups |
    Where-Object {
        $_.Name -eq "Finance"
    }

$financeGroup.Group |
    Select-Object -Property UserPrincipalName, Decision |
    Sort-Object -Property UserPrincipalName |
    Format-Table -AutoSize

This is useful for IAM investigations. A summary might show an unexpected concentration of pending decisions in one department; the .Group collection provides the records that explain the count.

If you only need summary counts and deliberately do not need drill-down records, -NoElement omits the Group collection:

$accessDecisions |
    Where-Object {
        $_.AccountState -eq "Enabled"
    } |
    Group-Object -Property Decision -NoElement |
    Sort-Object -Property Count -Descending |
    Select-Object -Property Name, Count |
    Format-Table -AutoSize

This yields an operational snapshot of enabled users by decision category. Because -NoElement discards the member collection from the resulting group objects, do not use it when you expect to investigate the underlying identities afterward.

A compact way to reason about this full report is:

  1. Filter to the population that matters.
  2. Group by the question you are asking.
  3. Sort the summaries by priority or volume.
  4. Select clear report fields.
  5. Format only for final human display.

Key takeaways

  • Where-Object keeps only records whose condition evaluates to $true; it normally passes through the original matching objects.
  • Prefer filtering at the source when supported, and otherwise filter early in the local pipeline.
  • Select-Object chooses or calculates report properties. Do not select away a property needed by a later filter, sort, or group.
  • Sort-Object changes order while preserving the population; use multiple properties for predictable IAM review queues.
  • Group-Object changes individual records into group-summary objects containing Name, Count, and, unless -NoElement is used, the underlying Group.
  • Keep Format-Table at the end of a display-only pipeline so prior stages continue to work with live objects.

Next, you will package reusable identity-processing logic into PowerShell functions with typed parameters, validation rules, and structured pipeline output.

Can't find a good explanation? Sign up and we'll make it for you

Sign up