What if your org chart wasn’t buried in HR PDFs but lived where access decisions are made?
Microsoft Entra ID supports a manager
property on each user object, which can be used to simulate an organizational chart—if populated correctly.
In this post, we’ll use PowerShell and Microsoft Graph to recursively build an org chart based on manager
relationships, then optionally export it to JSON or Graphviz DOT format for visualization.
🧱 What You’ll Learn #
- How to query user-manager relationships with Microsoft Graph
- How to recursively walk Entra ID’s hierarchy using PowerShell
- Where org chart data tends to break in the real world
- How to output to structured formats like JSON and DOT
⚙️ Prerequisites #
Install-Module Microsoft.Graph -Scope CurrentUser -Force
Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"
🧠 Key Graph Properties #
We’ll need:
Id
DisplayName
UserPrincipalName
Department
Manager
🔁 Recursive Org Chart Builder (PowerShell) #
function Get-OrgNode {
param (
[string]$UserId,
[int]$Depth = 0
)
$user = Get-MgUser -UserId $UserId -Property "Id,DisplayName,UserPrincipalName,Department" -ErrorAction SilentlyContinue
if (-not $user) { return }
$indent = " " * ($Depth * 4)
Write-Host "$indent├── $($user.DisplayName) [$($user.Department)]" -ForegroundColor Cyan
$directReports = Get-MgUserDirectReport -UserId $UserId -ErrorAction SilentlyContinue
foreach ($report in $directReports) {
Get-OrgNode -UserId $report.Id -Depth ($Depth + 1)
}
}
# Replace with the ID of your top-level manager (e.g., CEO or head of department)
$rootUser = Get-MgUser -Filter "displayName eq 'Charles Xavier'"
Get-OrgNode -UserId $rootUser.Id
The Result #
PS C:\Users\logphile>function Get-OrgNode {
param (
[string]$UserId,
[int]$Depth = 0
)
$user = Get-MgUser -UserId $UserId -Property "Id,DisplayName,UserPrincipalName,Department" -ErrorAction SilentlyContinue
if (-not $user) { return }
$indent = " " * ($Depth * 4)
Write-Host "$indent├── $($user.DisplayName) [$($user.Department)]" -ForegroundColor Cyan
$directReports = Get-MgUserDirectReport -UserId $UserId -ErrorAction SilentlyContinue
foreach ($report in $directReports) {
Get-OrgNode -UserId $report.Id -Depth ($Depth + 1)
}
}
# Replace with the ID of your top-level manager (e.g., CEO or head of department)
$rootUser = Get-MgUser -Filter "displayName eq 'Charles Xavier'"
Get-OrgNode -UserId $rootUser.Id
├── Charles Xavier [Leadership]
├── Ororo Munroe [Mutant Affairs]
├── Scott Summers [Field Operations]
├── Hisako Ichiki [Field Operations]
├── Warren Worthington [Finance]
├── Bobby Drake [Field Operations]
├── James Howlett [Security]
├── Laura Kinney [Security]
├── Piotr Nikolayevich Rasputin [Field Operations]
├── Kurt Wagner [Teleportation Ops]
├── Megan Gwynn [Teleportation Ops]
├── Clarice Ferguson [Teleportation Ops]
├── Hank McCoy [Science Division]
├── Forge [Science Division]
├── Tessa [Information Technology]
PS C:\Users\logphile>
📤 Export to JSON (Optional) #
function Build-OrgTreeJson {
param ([string]$UserId)
$user = Get-MgUser -UserId $UserId -Property "Id,DisplayName,UserPrincipalName,Department"
$directReports = Get-MgUserDirectReport -UserId $UserId
$children = foreach ($report in $directReports) {
Build-OrgTreeJson -UserId $report.Id
}
return [PSCustomObject]@{
Name = $user.DisplayName
UPN = $user.UserPrincipalName
Department = $user.Department
Reports = $children
}
}
# Get the root node (replace with your actual root if needed)
$rootUser = Get-MgUser -Filter "displayName eq 'Charles Xavier'"
# Build the tree
$tree = Build-OrgTreeJson -UserId $rootUser.Id
# Export to JSON
$tree | ConvertTo-Json -Depth 10 | Out-File ".\\orgTree.json" -Encoding utf8
{
"Name": "Charles Xavier",
"UPN": "[email protected]",
"Department": "Leadership",
"Reports": [
{
"Name": "Ororo Munroe",
"UPN": "[email protected]",
"Department": "Mutant Affairs",
"Reports": [
{
"Name": "Scott Summers",
"UPN": "[email protected]",
"Department": "Field Operations",
"Reports": {
"Name": "Hisako Ichiki",
"UPN": "[email protected]",
"Department": "Field Operations",
"Reports": null
}
},
{
"Name": "Warren Worthington",
"UPN": "[email protected]",
"Department": "Finance",
"Reports": null
},
{
"Name": "Bobby Drake",
"UPN": "[email protected]",
"Department": "Field Operations",
"Reports": null
},
{
"Name": "James Howlett",
"UPN": "[email protected]",
"Department": "Security",
"Reports": {
"Name": "Laura Kinney",
"UPN": "[email protected]",
"Department": "Security",
"Reports": null
}
},
{
"Name": "Piotr Nikolayevich Rasputin",
"UPN": "[email protected]",
"Department": "Field Operations",
"Reports": null
},
{
"Name": "Kurt Wagner",
"UPN": "[email protected]",
"Department": "Teleportation Ops",
"Reports": [
{
"Name": "Megan Gwynn",
"UPN": "[email protected]",
"Department": "Teleportation Ops",
"Reports": null
},
{
"Name": "Clarice Ferguson",
"UPN": "[email protected]",
"Department": "Teleportation Ops",
"Reports": null
}
]
}
]
},
{
"Name": "Hank McCoy",
"UPN": "[email protected]",
"Department": "Science Division",
"Reports": {
"Name": "Forge",
"UPN": "[email protected]",
"Department": "Science Division",
"Reports": null
}
},
{
"Name": "Tessa",
"UPN": "[email protected]",
"Department": "Information Technology",
"Reports": null
}
]
}
🧷 Where This Breaks #
- Missing
manager
field = orphaned node - Cycles (rare but possible in messy directories)
- Manager points to deactivated or deleted accounts
- Top-level user has no
manager
= must start with known name
🔍 Bonus: Graphviz DOT Export #
function Export-ToDot {
param ([object]$Node)
$lines = @()
foreach ($report in $Node.Reports) {
$lines += "`"{0}`" -> `"{1}`"" -f $Node.Name, $report.Name
$lines += Export-ToDot -Node $report
}
return $lines
}
$dot = @(
"digraph OrgChart {",
" node [shape=box style=filled color=\"#E3F2FD\" fontname=\"Segoe UI\" fontsize=10];",
" edge [arrowhead=vee color=\"#90CAF9\"];"
)
$dot += Export-ToDot -Node $tree
$dot += "}"
$dot -join "`n" | Out-File ".\\orgchart.dot"
Run New orgchart.dot Through GraphViz #
C:\Users\logphile>dot -Tpng orgchart.dot -o orgchart.png
The Results #

GraphViz can display data in a lot of cool ways. We can add color and more data.

📎 References #
Want your org chart to update itself? Use this with Azure Automation or GitHub Actions and post the output to Teams or SharePoint. Clean. Reusable. Always current.