Skip to content
ConsoleMonster

Microsoft 365 license usage

Put assigned against purchased for every SKU and price the seats nobody uses.

Demos Reports

Use it when a renewal is coming and you need to know which licenses are paid for but not assigned.

  • Static
  • 100×26
  • PowerShell 7.4+
  • .NET 8.0+
Microsoft 365 license usage 100×2624-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; all three styles produce the same frame bytes.

PowerShelllicense-usage.ps1View on GitHub
# Compare purchased and assigned Microsoft 365 licenses and price the unused seats.
Import-Module -Name ConsoleMonster

# Data: a fictional tenant with illustrative monthly prices; no Graph or admin API is called.
$skus = @(
    [pscustomobject] @{ Sku = 'Microsoft 365 E3'; Purchased = 450; Assigned = 431; Price = 36 }
    [pscustomobject] @{ Sku = 'Microsoft 365 E5'; Purchased = 120; Assigned = 74; Price = 57 }
    [pscustomobject] @{ Sku = 'Office 365 E1'; Purchased = 300; Assigned = 288; Price = 10 }
    [pscustomobject] @{ Sku = 'Exchange Online Plan 1'; Purchased = 80; Assigned = 63; Price = 4 }
    [pscustomobject] @{ Sku = 'Power BI Pro'; Purchased = 150; Assigned = 121; Price = 14 }
    [pscustomobject] @{ Sku = 'Visio Plan 2'; Purchased = 40; Assigned = 12; Price = 15 }
    [pscustomobject] @{ Sku = 'Project Plan 3'; Purchased = 25; Assigned = 24; Price = 30 }
)

# Behaviour: 90% assigned is healthy, under 70% is waste worth reclaiming; waste is unused seats × price.
$usage = foreach ($sku in $skus) {
    $percent = [math]::Round(100 * $sku.Assigned / $sku.Purchased)
    $status = if ($percent -ge 90) { 'Pass' } elseif ($percent -ge 70) { 'Warn' } else { 'Fail' }
    $unused = $sku.Purchased - $sku.Assigned
    [pscustomobject] @{ Sku = $sku.Sku; Purchased = $sku.Purchased; Assigned = $sku.Assigned
        Unused = $unused; 'Used %' = $percent; 'Waste $' = $unused * $sku.Price; Status = $status }
}
$reclaim = @($usage |
    Where-Object -Property Status -NE -Value 'Pass' |
    Sort-Object -Property 'Waste $' -Descending |
    Select-Object -First 3)
$unusedSeats = ($usage | Measure-Object -Property Unused -Sum).Sum
$monthlyWaste = ($usage | Measure-Object -Property 'Waste $' -Sum).Sum

# Layout: utilisation bars beside the biggest savings, then the full ledger.
$report = New-ConsoleRows -Gap 1 -Content {
    New-ConsoleRule -Title '[bold]Microsoft 365 licenses[/] [muted]· adatum.example.test · October 2026[/]'
    New-ConsoleColumns -Content {
        New-ConsolePanel -Title 'Assigned of purchased' -Content {
            $usage |
                ForEach-Object -Process {
                    [pscustomobject] @{ Label = $_.Sku; Value = $_.'Used %'; Role = $_.Status }
                } |
                    New-ConsoleChart -Type Bar -Maximum 100 -Unit '%'
        }
        New-ConsolePanel -Title 'Reclaim first' -Content {
            New-ConsoleStatusList -Content {
                foreach ($sku in $reclaim) {
                    [pscustomobject] @{ Label = $sku.Sku; Status = $sku.Status
                        Detail = ('{0} unused · ${1:N0}/mo' -f $sku.Unused, $sku.'Waste $') }
                }
            }
        }
    }
    New-ConsoleTable -InputObject $usage -Property Sku, Purchased, Assigned, Unused, 'Used %',
        'Waste $', Status -Expand
    ('[warn]${0:N0} a month[/] pays for {1} unassigned seats. [muted]Reclaim before the renewal date.[/]' -f $monthlyWaste,
        $unusedSeats)
}

# Behaviour: print once; review the reclaim list with each license owner before removing seats.
$report | Show-Console
C#Program.csView on GitHub
// Compare purchased and assigned Microsoft 365 licenses and price the unused seats.
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: a fictional tenant with illustrative monthly prices; no Graph or admin API is called.
var skus = new Sku[]
{
    new("Microsoft 365 E3", 450, 431, 36),
    new("Microsoft 365 E5", 120, 74, 57),
    new("Office 365 E1", 300, 288, 10),
    new("Exchange Online Plan 1", 80, 63, 4),
    new("Power BI Pro", 150, 121, 14),
    new("Visio Plan 2", 40, 12, 15),
    new("Project Plan 3", 25, 24, 30)
};

// Behaviour: 90% assigned is healthy, under 70% is waste worth reclaiming; waste is unused seats × price.
var usage = skus.Select(sku =>
{
    int percent = (int)Math.Round(100.0 * sku.Assigned / sku.Purchased);
    StatusKind status = percent >= 90
        ? StatusKind.Pass
        : percent >= 70 ? StatusKind.Warn : StatusKind.Fail;
    int unused = sku.Purchased - sku.Assigned;
    return new Usage(sku.Name, sku.Purchased, sku.Assigned, unused, percent, unused * sku.Price,
        status);
}).ToArray();
var reclaim = usage.Where(sku => sku.Status != StatusKind.Pass)
    .OrderByDescending(sku => sku.Waste)
    .Take(3);
int unusedSeats = usage.Sum(sku => sku.Unused);
int monthlyWaste = usage.Sum(sku => sku.Waste);

// Layout: utilisation bars beside the biggest savings, then the full ledger.
var bars = new BarChart { Maximum = 100, Unit = "%" };
foreach (var sku in usage)
    bars.Add(sku.Sku, sku.Used, sku.Status.ToToken());
var savings = new StatusList();
foreach (var sku in reclaim)
    savings.Add(sku.Sku, sku.Status, $"{sku.Unused} unused · ${sku.Waste:N0}/mo");

var table = Table.For(usage)
    .Column(row => row.Sku)
    .Column(row => row.Purchased)
    .Column(row => row.Assigned)
    .Column(row => row.Unused)
    .Column("Used %", row => row.Used)
    .Column("Waste $", row => row.Waste)
    .Column(row => row.Status, column => column.Presenter = CellPresenters.StatusWord);
table.Expand = true;

var report = new Rows(
    new Rule("[bold]Microsoft 365 licenses[/] [muted]· adatum.example.test · October 2026[/]"),
    new Columns(new Panel(bars, "Assigned of purchased"), new Panel(savings, "Reclaim first")),
    table,
    new TextBlock(
        ($"[warn]${monthlyWaste:N0} a month[/] pays for {unusedSeats} unassigned seats. "
            + $"[muted]Reclaim before the renewal date.[/]")))
{
    Gap = 1
};

// Behaviour: print once; review the reclaim list with each license owner before removing seats.
Terminal.Write(report);

record Sku(string Name, int Purchased, int Assigned, int Price);

record Usage(string Sku, int Purchased, int Assigned, int Unused, int Used, int Waste,
    StatusKind Status);
C# fluentProgram.Fluent.csView on GitHub
// Compare purchased and assigned Microsoft 365 licenses and price the unused seats.
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: a fictional tenant with illustrative monthly prices; no Graph or admin API is called.
var skus = new Sku[]
{
    new("Microsoft 365 E3", 450, 431, 36),
    new("Microsoft 365 E5", 120, 74, 57),
    new("Office 365 E1", 300, 288, 10),
    new("Exchange Online Plan 1", 80, 63, 4),
    new("Power BI Pro", 150, 121, 14),
    new("Visio Plan 2", 40, 12, 15),
    new("Project Plan 3", 25, 24, 30)
};

// Behaviour: 90% assigned is healthy, under 70% is waste worth reclaiming; waste is unused seats × price.
var usage = skus.Select(sku =>
{
    int percent = (int)Math.Round(100.0 * sku.Assigned / sku.Purchased);
    StatusKind status = percent >= 90
        ? StatusKind.Pass
        : percent >= 70 ? StatusKind.Warn : StatusKind.Fail;
    int unused = sku.Purchased - sku.Assigned;
    return new Usage(sku.Name, sku.Purchased, sku.Assigned, unused, percent, unused * sku.Price,
        status);
}).ToArray();
var reclaim = usage.Where(sku => sku.Status != StatusKind.Pass)
    .OrderByDescending(sku => sku.Waste)
    .Take(3);
int unusedSeats = usage.Sum(sku => sku.Unused);
int monthlyWaste = usage.Sum(sku => sku.Waste);

// Layout: utilisation bars beside the biggest savings, then the full ledger.
var bars = (BarChart)Ui.Chart().Type(ChartType.Bar).Maximum(100).Unit("%").Build();
foreach (var sku in usage)
    bars.Add(sku.Sku, sku.Used, sku.Status.ToToken());
var savings = Ui.StatusList();
foreach (var sku in reclaim)
    savings.Add(sku.Sku, sku.Status, $"{sku.Unused} unused · ${sku.Waste:N0}/mo");
var table = Ui.Table(usage)
    .Column(row => row.Sku)
    .Column(row => row.Purchased)
    .Column(row => row.Assigned)
    .Column(row => row.Unused)
    .Column("Used %", row => row.Used)
    .Column("Waste $", row => row.Waste)
    .Column(row => row.Status, column => column.Presenter(CellPresenters.StatusWord))
    .Expand();
var report = Ui.Rows().Gap(1).Content(c3 =>
{
    c3.Rule("[bold]Microsoft 365 licenses[/] [muted]· adatum.example.test · October 2026[/]");
    c3.Columns(columns => columns.Content(c2 =>
    {
        c2.Panel(p => p.Title("Assigned of purchased").Content(c =>
        {
            c.Add(bars);
        }));
        c2.Panel(p => p.Title("Reclaim first").Content(c =>
        {
            c.Add(savings);
        }));
    }));
    c3.Add(table);
    c3.Markup(($"[warn]${monthlyWaste:N0} a month[/] pays for {unusedSeats} unassigned seats. "
        + $"[muted]Reclaim before the renewal date.[/]"));
});

// Behaviour: print once; review the reclaim list with each license owner before removing seats.
report.Show();

record Sku(string Name, int Purchased, int Assigned, int Price);

record Usage(string Sku, int Purchased, int Assigned, int Unused, int Used, int Waste,
    StatusKind Status);

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into license-usage.ps1.

  3. Run it

    From the folder you saved it in.

    ./license-usage.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n LicenseUsage; cd LicenseUsage
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n LicenseUsage; cd LicenseUsage
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run --property:DemoStyle=Fluent

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo license-usage
Export-ConsoleDemo license-usage -Path ./license-usage.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo license-usage -n LicenseUsage
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo license-usage --DemoStyle Fluent -n LicenseUsage

Make it yours

Start with these, then read the rest of the file.

  1. Replace the inline SKUs with your tenant's subscribed SKUs and their consumed units.

  2. Set each SKU's price from your agreement and change the 90% and 70% thresholds.

  3. Add an owner per SKU so the reclaim list names who to ask.

More reports

All demos