I had to create a number of Visio diagrams for a new project. Since Blender has a Python API, I wondered if I could do something similar with Visio. There does appear to be an VSDX library for Python, I also found that Powershell can just control the Visio instance on my laptop.
This is a demo creating a diagram for a simple web server with a database back end. You can, however, use any stencils and make more complicated diagrams. The lines aren’t great — part of my Visio diagramming process is moving things around to optimize placement to avoid overlapping and confusing lines. The programmatic approach doesn’t do that, but it gets everything in the diagram. You can then move them as needed.
# Sample Visio diagram: Firewall -> Load Balancer -> Web Servers -> Database
# Auto-discovers stencils
# Works on Windows PowerShell 5.x
$ErrorActionPreference = "Stop"
# Output
$docName = "WebApp-LB-Firewall-DB.vsdx"
$outPath = Join-Path $HOME "Documents\$docName"
# Start Visio
$visio = New-Object -ComObject Visio.Application
$visio.Visible = $true
# New document/page
$doc = $visio.Documents.Add("")
$page = $visio.ActivePage
$page.Name = "Architecture"
$page.PageSheet.CellsU("PageWidth").ResultIU = 22.0
$page.PageSheet.CellsU("PageHeight").ResultIU = 14.0
# -------------------------------
# Stencil discovery and loading
# -------------------------------
$searchRoots = @(
"$env:PROGRAMFILES\Microsoft Office\root\Office16\Visio Content",
"$env:PROGRAMFILES\Microsoft Office\root\Office16\Visio Content\1033",
"$env:ProgramFiles(x86)\Microsoft Office\root\Office16\Visio Content",
"$env:ProgramFiles(x86)\Microsoft Office\root\Office16\Visio Content\1033",
"$env:PROGRAMFILES\Microsoft Office\root\Office15\Visio Content",
"$env:ProgramFiles(x86)\Microsoft Office\root\Office15\Visio Content",
"$env:PROGRAMFILES\Microsoft",
"$env:ProgramFiles(x86)\Microsoft",
"$env:PROGRAMFILES",
"$env:ProgramFiles(x86)"
) | Where-Object { Test-Path $_ }
# Keywords to select useful stencils (filename match, case-insensitive)
$stencilKeywords = @("network","server","compute","computer","azure","cloud","firewall","security","database","sql","load","balancer","web","iis")
function Find-StencilFiles {
param([string[]]$roots, [string[]]$keywords)
$results = @()
foreach ($root in $roots) {
try {
Get-ChildItem -Path $root -Filter *.vssx -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
$fname = $_.Name.ToLower()
foreach ($kw in $keywords) {
if ($fname -match $kw) { $results += $_.FullName; break }
}
}
} catch { }
}
$results | Select-Object -Unique
}
function Load-Stencils {
param([string[]]$files)
$loaded = @()
foreach ($file in $files) {
try {
Write-Host "Loading stencil: $file"
$loaded += $visio.Documents.OpenEx($file, 64) # read-only
} catch {
Write-Warning "Could not load stencil: $file"
}
}
foreach ($docX in $visio.Documents) {
if ($docX.FullName -ne $doc.FullName) { $loaded += $docX }
}
$loaded | Sort-Object FullName -Unique
}
$files = Find-StencilFiles -roots $searchRoots -keywords $stencilKeywords
$stencils = Load-Stencils -files $files
if (!$stencils -or $stencils.Count -eq 0) {
Write-Warning "No stencil files loaded automatically. Fallback rectangles will be used."
} else {
Write-Host "`nLoaded stencils:" -ForegroundColor Cyan
foreach ($s in $stencils) { Write-Host " - $($s.FullName)" }
}
# -------------------------------
# Master selection helpers
# -------------------------------
function List-Masters {
foreach ($st in $stencils) {
Write-Host ("Stencil/Doc: {0}" -f $st.Name) -ForegroundColor Cyan
foreach ($m in $st.Masters) {
Write-Host (" - {0} (NameU: {1})" -f $m.Name, $m.NameU)
}
}
}
function Get-MasterByPattern([string[]]$patterns) {
foreach ($st in $stencils) {
foreach ($m in $st.Masters) {
foreach ($p in $patterns) {
if ($m.NameU -match $p -or $m.Name -match $p) {
Write-Host ("Selected master '{0}' from '{1}' for pattern '{2}'" -f $m.Name, $st.Name, $p) -ForegroundColor Green
return $m
}
}
}
}
return $null
}
# Drop master centered at x,y; keep default size; label it
function Add-Device([double]$x,[double]$y,[string]$label,[string[]]$patterns,[double]$fontSize=10) {
$m = Get-MasterByPattern $patterns
if ($null -eq $m) {
Write-Warning ("No master matched patterns: {0}. Using fallback rectangle." -f ($patterns -join ", "))
$w = 2.0; $h = 1.2
$shape = $page.DrawRectangle($x - ($w/2), $y - ($h/2), $x + ($w/2), $y + ($h/2))
} else {
$shape = $page.Drop($m, $x, $y)
}
$shape.Text = $label
$shape.CellsU("Char.Size").FormulaU = "$fontSize pt"
return $shape
}
# Simple transparent containers (thin gray outline; sent behind shapes)
function Add-Container([double]$x,[double]$y,[double]$w,[double]$h,[string]$text) {
$shape = $page.DrawRectangle($x, $y, $x + $w, $y + $h)
$shape.CellsU("LineColor").FormulaU = "RGB(180,180,180)"
$shape.CellsU("LineWeight").FormulaU = "1 pt"
$shape.CellsU("FillForegnd").FormulaU = "RGB(255,255,255)"
$shape.CellsU("FillForegndTrans").ResultIU = 1.0
$shape.Text = $text
$shape.CellsU("Char.Size").FormulaU = "12 pt"
try { $shape.SendToBack() } catch {}
return $shape
}
# Connector
function Connect($fromShape,$toShape,[string]$text="") {
$conn = $page.Drop($visio.Application.ConnectorToolDataObject, 0, 0)
$conn.CellsU("LineColor").FormulaU = "RGB(60,60,60)"
$conn.CellsU("LineWeight").FormulaU = "0.75 pt"
$fromShape.AutoConnect($toShape, 0, $conn)
if ($text) { $conn.Text = $text }
return $conn
}
# -------------------------------
# Diagram content
# -------------------------------
# Title
$title = $page.DrawRectangle(1.0, 13.4, 21.0, 13.9)
$title.Text = "Web App Architecture: Firewall -> Load Balancer -> Web Servers -> Database"
$title.CellsU("Char.Size").FormulaU = "14 pt"
# Patterns for official icons (broad to match common stencils)
$patFirewall = @("Firewall|Security|Shield|Azure.*Firewall")
$patLoadBalancer= @("Load.*Balancer|Application.*Gateway|LB|Azure.*Load.*Balancer")
$patWebServer = @("Web.*Server|IIS|Server(?! Rack)|Computer|Windows.*Server")
$patDatabase = @("Database|SQL|Azure.*SQL|DB|Cylinder")
# Containers (optional zones)
$dmz = Add-Container 1.0 10.8 20.0 2.0 "DMZ (Edge/Ingress)"
$webtier = Add-Container 4.0 6.8 14.0 3.2 "Web Tier"
$dbtier = Add-Container 8.0 3.5 10.0 2.8 "Database Tier"
$clients = Add-Container 1.0 1.0 6.0 2.2 "Clients"
# Devices (kept at native size; spaced widely)
# Edge/Ingress
$fw = Add-Device 3.0 11.8 "Firewall" $patFirewall 10
$lb = Add-Device 8.0 11.8 "Load Balancer" $patLoadBalancer 10
# Web servers (pair)
$web1 = Add-Device 9.5 8.0 "Web Server 1\nIIS" $patWebServer 10
$web2 = Add-Device 13.5 8.0 "Web Server 2\nIIS" $patWebServer 10
# Database
$db = Add-Device 13.0 4.6 "Database\nSQL" $patDatabase 10
# Clients
$client1 = Add-Device 2.0 1.8 "Client\nPC" @("Desktop|PC|Computer|Laptop") 10
$client2 = Add-Device 5.0 1.8 "Client\nServer" @("Server(?! Rack)|Windows.*Server|Computer") 10
# Connectors (flow: clients -> firewall -> LB -> web servers -> database)
Connect $client1 $fw "HTTPS"
Connect $client2 $fw "HTTPS"
Connect $fw $lb "Allow: 443"
Connect $lb $web1 "HTTP/HTTPS"
Connect $lb $web2 "HTTP/HTTPS"
Connect $web1 $db "SQL (1433/Encrypted)"
Connect $web2 $db "SQL (1433/Encrypted)"
# Save
$doc.SaveAs($outPath)
Write-Host "Saved Visio to: $outPath"