
Teams Call Flow Visualizer
Auto Attendants · Call Queues · Users
See your Teams call flow, instantly
Connect to Teams via PowerShell, drop the exported JSON in here, and get an interactive diagram of every Auto Attendant, Call Queue and routing target.
Files are parsed locally in your browser — nothing is uploaded.
Step 1 · Run this in PowerShell
One script. Exports resource accounts, auto attendants, call queues, business-hour schedules, holidays, greetings, timers and per-user forwarding to teams-callflow.json.
# Run in PowerShell as a Teams admin — produces a single teams-callflow.json file
# with full call-flow detail: schedules, holidays, greetings, timers, user forwarding.
if (-not (Get-Module -ListAvailable -Name MicrosoftTeams)) {
Install-Module MicrosoftTeams -Scope CurrentUser -Force
}
Import-Module MicrosoftTeams
Connect-MicrosoftTeams | Out-Null
Write-Host "Fetching Resource Accounts..."
$appsRaw = @(Get-CsOnlineApplicationInstance -ErrorAction SilentlyContinue)
Write-Host "Fetching Phone Number Assignments..."
$assignments = @()
try {
$assignments = @(Get-CsPhoneNumberAssignment -ErrorAction Stop)
} catch {
Write-Warning "Get-CsPhoneNumberAssignment failed: $($_.Exception.Message)"
}
# Build lookup using every identifier Teams may expose for the Resource Account.
$phoneByTarget = @{}
foreach ($pn in $assignments) {
$num = $pn.TelephoneNumber
if (-not $num) { continue }
foreach ($candidate in @(
$pn.AssignedPstnTargetId, $pn.AssignedPstnTargetName, $pn.PstnAssignmentId,
$pn.Identity, $pn.TargetId, $pn.TargetName, $pn.UserPrincipalName
)) {
if ($candidate) { $phoneByTarget[$candidate.ToString().ToLower()] = $num }
}
}
# Merge phone numbers back onto application instances so the visualizer sees them
$apps = $appsRaw | ForEach-Object {
$obj = $_
$resolved = $obj.PhoneNumber
foreach ($candidate in @($obj.ObjectId,$obj.Identity,$obj.Id,$obj.UserPrincipalName,$obj.DisplayName)) {
if ($resolved) { break }
if ($candidate) {
$key = $candidate.ToString().ToLower()
if ($phoneByTarget.ContainsKey($key)) { $resolved = $phoneByTarget[$key] }
}
}
$obj | Add-Member -NotePropertyName PhoneNumber -NotePropertyValue $resolved -Force -PassThru
}
Write-Host "Fetching Auto Attendants (with full call-flow detail)..."
$aas = @(Get-CsAutoAttendant -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)
Write-Host "Fetching Call Queues (with full settings)..."
$cqs = @(Get-CsCallQueue -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)
Write-Host "Fetching Schedules (business hours + holidays)..."
$schedules = @()
try {
$schedules = @(Get-CsOnlineSchedule -ErrorAction Stop -WarningAction SilentlyContinue)
} catch {
Write-Warning "Get-CsOnlineSchedule failed: $($_.Exception.Message)"
}
Write-Host "Fetching Holidays per Auto Attendant..."
# Get-CsAutoAttendantHolidays REQUIRES -Identity — iterate per AA so it
# never prompts. Holiday schedules are also exposed via Get-CsOnlineSchedule
# above, so this is best-effort enrichment only.
$holidays = @()
foreach ($aa in $aas) {
if (-not $aa.Identity) { continue }
try {
$h = Get-CsAutoAttendantHolidays -Identity $aa.Identity -ErrorAction Stop -WarningAction SilentlyContinue
if ($h) { $holidays += $h }
} catch { continue }
}
Write-Host "Fetching Users + Calling Settings..."
$users = @(Get-CsOnlineUser -ResultSize 2000 -ErrorAction SilentlyContinue |
Select-Object Identity,DisplayName,UserPrincipalName,LineUri,UsageLocation,
Department,JobTitle,EnterpriseVoiceEnabled)
# ---- Resolve every callable target ID to a human-friendly entity ---------
# Teams stores menu options / overflow / timeout / operator targets as a
# CallableEntity { Type; Id }. The Id is usually a GUID (User or
# ApplicationEndpoint) or "tel:+32..." (ExternalPstn). We pre-resolve every
# GUID we know about so the visualizer can show the actual phone number,
# display name and UPN — never a raw GUID.
$entityResolutions = @()
foreach ($a in $apps) {
$entityResolutions += [pscustomobject]@{
Id = ($a.ObjectId | Out-String).Trim()
Identity = ($a.Identity | Out-String).Trim()
Kind = "ApplicationInstance"
DisplayName = $a.DisplayName
UserPrincipalName = $a.UserPrincipalName
PhoneNumber = $a.PhoneNumber
}
}
foreach ($u in $users) {
$entityResolutions += [pscustomobject]@{
Id = ($u.Identity | Out-String).Trim()
Identity = ($u.Identity | Out-String).Trim()
Kind = "User"
DisplayName = $u.DisplayName
UserPrincipalName = $u.UserPrincipalName
PhoneNumber = ($u.LineUri -replace '^tel:','' -replace ';.*$','')
Department = $u.Department
JobTitle = $u.JobTitle
}
}
# Distribution lists / groups referenced by call queues — resolve via Graph
# fallback (best-effort; missing groups are skipped silently).
$groupIds = @()
foreach ($cq in $cqs) {
if ($cq.DistributionLists) { $groupIds += @($cq.DistributionLists) }
}
$groupIds = $groupIds | Select-Object -Unique
foreach ($gid in $groupIds) {
if (-not $gid) { continue }
try {
$g = Get-CsGroup -Identity $gid -ErrorAction Stop
$entityResolutions += [pscustomobject]@{
Id = $gid
Identity = $gid
Kind = "Group"
DisplayName = $g.DisplayName
}
} catch { }
}
# Per-user forwarding / simultaneous-ring / call group settings — non-interactive
$userCallSettings = @()
$total = $users.Count
$i = 0
foreach ($u in $users) {
$i++
$upn = $u.UserPrincipalName
if ([string]::IsNullOrWhiteSpace($upn)) { continue }
if ($upn -notmatch '@') { continue } # skip anything that wouldn't be a valid identity
if ($i % 25 -eq 0) { Write-Host " ...calling settings $i / $total" }
try {
$s = Get-CsUserCallingSettings -Identity $upn -ErrorAction Stop -WarningAction SilentlyContinue -Confirm:$false
if (-not $s) { continue }
$userCallSettings += [pscustomobject]@{
UserPrincipalName = $upn
Identity = $u.Identity
IsForwardingEnabled = $s.IsForwardingEnabled
ForwardingType = $s.ForwardingType
ForwardingTargetType = $s.ForwardingTargetType
ForwardingTarget = $s.ForwardingTarget
IsUnansweredEnabled = $s.IsUnansweredEnabled
UnansweredDelay = $s.UnansweredDelay
UnansweredTargetType = $s.UnansweredTargetType
UnansweredTarget = $s.UnansweredTarget
Delegates = $s.Delegates
CallGroupOrder = $s.CallGroupOrder
CallGroupTargets = $s.CallGroupTargets
SimRingTargetType = $s.SimRingTargetType
SimRingTarget = $s.SimRingTarget
}
} catch {
# silently skip users we can't read (guests, service accounts, no Teams licence)
continue
}
}
$export = [ordered]@{
exportedAt = (Get-Date).ToString("o")
microsoftTeamsModule = (Get-Module MicrosoftTeams).Version.ToString()
applicationInstances = $apps
phoneNumberAssignments = $assignments
autoAttendants = $aas
callQueues = $cqs
schedules = $schedules
holidays = $holidays
users = $users
userCallingSettings = $userCallSettings
entityResolutions = $entityResolutions
}
$export | ConvertTo-Json -Depth 20 | Out-File -Encoding utf8 teams-callflow.json
Write-Host "Done — upload teams-callflow.json to the visualizer."
Requires Teams admin rights. The file is created in your current working directory.
Step 2 · Upload teams-callflow.json
Drop the file in. It's parsed locally — nothing leaves your browser.
Inbound numbers
Resource accounts and PSTN entry points.
Routing logic
Menus, DTMF options, overflow & timeout actions.
Local-first
Your tenant data never leaves the browser.