You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adding an Excel export option to AsBuiltReport is something I've wanted to explore for a long time. The PScribo module currently handles all formatting within the framework, but it appears to no longer be actively maintained (I'll leave that for @iainbrighton to confirm). While I'm familiar with ImportExcel and its capabilities, I've been reluctant to introduce another module dependency into the framework.
AI tooling has now made it practical to revisit this idea. I've been using Claude Code heavily over the past 12 months for code development and have used it to assess the feasibility of adding Excel export to ABR. The plan is to build a proof of concept in two stages. First, use ImportExcel to validate that Excel output can be generated from existing report modules, then develop a native PScribo plugin so that PScribo remains the sole formatting tool.
The plan below was drafted with Claude's assistance. A proof of concept will begin soon and feedback on its requirements and direction is welcomed as it progresses.
Important
This is not a commitment to delivering this functionality. This is purely a discussion to explore the idea.
AsBuiltReport — XLSX Export Feasibility & Implementation Plan
Version: 0.1.0 Date: 2026年06月04日 Scope: Add Excel (.xlsx) export to the AsBuiltReport framework using the ImportExcel PowerShell module, where document sections map to worksheets. Modules analysed:AsBuiltReport.Core (v1.6.2), PScribo (v0.12.0 dev / v0.11.1 min), and report modules VMware.vSphere, Microsoft.Azure, NetApp.ONTAP, Veeam.VBR.
---
1. Verdict
Adding XLSX export is feasible and low-risk to the existing Word/HTML/Text outputs, because every report already builds a fully-populated, traversable PScribo document object model in memory before export. We can walk that object tree and emit a workbook with ImportExcel without touching any report module.
Worksheet mapping is settled: each Heading2 section becomes its own worksheet. This is the correct rule because every report emits exactly one Heading1 section (the target name — vCenter, tenant, cluster, backup server), while the meaningful "chapters" a user expects as tabs (Clusters, Datastores, VMHosts, Networks, Subscriptions, ...) are Heading2 sections. Heading1 is carried as a workbook title / "Report Info" sheet. Implemented via a configurable WorksheetHeadingLevel defaulting to 2. See §4.
2. How report generation works today (the integration point)
New-AsBuiltReport (AsBuiltReport.Core/Src/Public/New-AsBuiltReport.ps1) does the following:
Builds the document with PScribo's Document keyword (lines ~524 and ~572). The scriptblock dot-sources the style script and invokes the report module's Invoke-AsBuiltReport.<Module>, which emits Section / Table / Paragraph calls.
The Document { ... } call returns the in-memory document object into $AsBuiltReport.
$AsBuiltReport is the hook. It is a fully-built PScribo.Document with a populated .Sections tree beforeExport-Document runs. We can pass that same object to a new Excel exporter. No report module changes are required, ever.
The -Format parameter is constrained here:
# New-AsBuiltReport.ps1 : line 237
\[ValidateSet('Word','HTML','Text')]
\[Array] $Format='Word',
---
3. Feasibility findings (verified against source)
3.1 The document object model is traversable and complete
Object
TypeName
Key properties
Root
PScribo.Document
.Sections (ArrayList of children), .Name, .Options
Heading level is derived (Level = Number.Split('.').Count - 1), so a recursive walk can classify any node as H1/H2/H3/... reliably. (Source: PScribo/Src/Private/New-PScriboSection.ps1, Invoke-PScriboSectionLevel.ps1.)
3.2 Table data is fully preserved as objects
Each PScribo.Table.Rows entry is a PSCustomObject whose properties are the column values — i.e. exactly the shape ImportExcel's Export-Excel consumes. We do not need to parse rendered text. Three layouts exist and each needs handling:
Standard (IsList = $false): rows ×ばつ columns → direct Export-Excel.
List (IsList = $true): vertical key/value (one source object). → emit as a 2-column Property/Value block, or transpose.
Keyed list (IsKeyedList = $true, .ListKey): grouped key/value. → group blocks.
3.3 Health-check cell styling is available as metadata (verified)
Set-Style -Style Critical/Warning/Info/OK \[-Property Col1,Col2] attaches note-properties to each row object:
\_\_Style — row-level style name
<ColumnName>\_\_Style — per-cell style name
Confirmed in PScribo/Src/Private/New-PScriboTableRow.ps1 (lines 43–77) and Src/Public/Set-Style.ps1 (lines 44–45). The Word/HTML plugins read these to colour cells; our Excel exporter can read the identical properties and apply background fills via ImportExcel. The Text plugin simply excludes \*\_\_Style columns — we do the same for data, but inspect them for formatting.
The style → colour map already exists in AsBuiltReport.Core/AsBuiltReport.Core.Style.ps1:
Style
Hex (background)
Excel use
Critical
FEDDD7
cell/row fill
Warning
FFF4C7
cell/row fill
Info
E3F5FC
cell/row fill
OK
DFF0D0
cell/row fill
TableDefaultHeading
fill 072E58, text FAFAFA
header row
These hex values can be lifted directly into ImportExcel -BackgroundColor / conditional formatting, giving the spreadsheet the same visual health-check cues as the Word report.
3.4 Content that does NOT map to cells
Roughly 40–60% of report content is tabular; the rest is paragraphs, blank lines, images/diagrams (NetApp & Veeam are diagram-heavy), and the cover page/TOC. These have no spreadsheet representation. Strategy in §4.4.
---
4. Design
4.1 The worksheet-mapping decision (most important)
Because reports have a single H1, choose one of:
Option
Worksheets
Pros
Cons
A. H2 → worksheet (recommended)
One tab per major chapter (Clusters, Datastores, VMHosts, Networks...)
Matches user mental model; clean tab list; natural for multi-target via prefix
H1 (target name) becomes a title/summary sheet, not a tab
B. Literal H1 → worksheet
One tab for the whole report
Trivial
Useless — single tab
C. Configurable depth (WorksheetHeadingLevel, default 2)
Author-tunable per report
Most flexible; future-proof
Slightly more config surface
Recommendation: implement Option C with a default of level 2 (so out-of-the-box behaviour = Option A). This honours the spirit of the brief ("a section per worksheet") while producing a usable workbook, and lets dense reports push to level 3 if desired.
4.1a Multi-system output: one workbook per system (recommended default, not a hard requirement)
Report modules loop over -Target internally and emit one Heading1 section per system within a single document object. Verified in Invoke-AsBuiltReport.VMware.vSphere.ps1:
foreach ($VIServerin$Target) { # line 41
...
Section -Style Heading1 $vCenterServerName { ... } # line 255 — one H1 per system
}
So a two-vCenter run yields a document whose .Sections contains two Level 0 sections. Both behaviours are feasible — the choice is cosmetic, not technical:
One workbook per system (recommended default). Split on Heading1; each H1 → its own .xlsx, named after that system, with that system's H2 sections as tabs. More logical for per-system analysis and keeps tab lists short.
Single combined workbook (alternative). All systems' H2 sections as tabs in one file, tab names prefixed with the system (<System> - <H2>).
Since it isn't a hard requirement, treat this as a switch (e.g. -ExcelPerSystem, default on) so either is trivially available. Note Word/HTML/Text always produce a single combined file (PScribo's model); per-system splitting is Excel-only and lives in Core (§4.5).
File naming (per-system mode):
Single system:<FileName>.xlsx — unchanged from today's naming ($ReportConfig.Report.Name, plus -Timestamp suffix if set).
Multiple systems: one file each, <FileName> - <SystemName>.xlsx, <SystemName> = sanitised H1 text (invalid filename chars stripped, de-duplicated on collision); -Timestamp still applies per file.
4.2 Heading-level disposition (H1–H6) — how the tree flattens
Report modules use heading levels 1–6. They map to Excel as follows (default WorksheetHeadingLevel = 2):
Level
Disposition
H1
File boundary — one .xlsx per H1 (= per system; see §4.1a).
H2
Worksheet boundary — one tab per H2.
H3–H6
In-sheet section banners — not tabs, not files. Each becomes a heading/label row inside its ancestor H2 worksheet, titling and grouping the table(s) beneath it.
H3–H6 never create new tabs or files; they are flattened into the H2 worksheet. For each H2, the exporter recurses the entire subtree, collects every PScribo.Table descendant in document order, and stacks them vertically down the sheet. Depth (H3 vs H4 vs H5 vs H6) is preserved visually, not structurally:
Banner row before each table from the owning sub-section's .Name, prefixed with its .Number (e.g. 1.4.2 SCSI LUN Info, which already encodes full hierarchy).
Indentation — banner (and optionally the table) shifts one column right per level (H3→A, H4→B, ...).
Style — banner font size/bold/colour mirrors the Style.ps1 heading palette (Heading 3=395879, Heading 4=958026, Heading 5=009684, Heading 6=009683). NO TOC HeadingN variants are treated by their numeric level (the "NO TOC" only affected Word/HTML TOC inclusion).
One blank row between stacked tables; freeze each table's header row; auto-size columns; apply an Excel table style per block.
Drop \*\_\_Style helper columns from the visible data; use them only for cell fill colour (§3.3).
Example — vSphere Datastores (H2) tab at InfoLevel 3+:
Datastores ← H2 = the worksheet/tab name
\[ summary table: all datastores ] ← table directly under H2
datastore-01 ← H3 banner (1.4.1)
\[ detail list table ]
SCSI LUN Info ← H4 banner (1.4.1.1)
\[ LUN table ]
datastore-02 ← H3 banner (1.4.2)
\[ detail list table ]
Optional (Phase 3): map heading depth to Excel row outline levels, making H3–H6 bands collapsible/expandable groups — a natural fit for the hierarchy, straightforward with ImportExcel.
If the knob is changed (WorksheetHeadingLevel = N): H1 always remains the file split; level N becomes the tab boundary; levels between H1 and N qualify (prefix) the tab name; levels belowN become the in-sheet banners above. At the default N = 2 this reduces to the table above.
This preserves the report's hierarchy as readable bands within a single tab, rather than exploding into dozens of micro-tabs.
4.3 Worksheet naming & sanitisation
Excel tab constraints must be enforced centrally:
Max 31 chars; strip \[ ] : \* ? / \\; cannot be blank; must be unique (append (2), (3) on collision); cannot be History.
Derive from Section.Name; keep a "section number → tab name" map for an optional index/TOC sheet with hyperlinks.
4.4 Non-tabular content
Paragraphs: optionally written as wrapped text rows above the first table of a sheet (off by default to keep sheets data-clean), or skipped.
Images/diagrams: skipped (optionally noted as "see Word/HTML report"). Embedding base64 images is a possible later enhancement via Add-ExcelImage-style logic, but out of MVP scope.
Cover page / TOC: replaced by a generated "Report Info" first sheet (Author, Company, Version, Date — same data the cover page Table already holds) plus an "Index" sheet linking to each tab.
4.5 Architecture: the exporter lives in Core and uses ImportExcel
A Core-resident exporter that consumes the $AsBuiltReport document object directly and renders with ImportExcel is the right home. It is self-contained, shippable without touching any other repo, and the natural place for AsBuiltReport-specific semantics (H2→worksheet, optional one-workbook-per-system, system-based filenames) that have no equivalent in generic PScribo. This is the basis for the MVP.
**Out of scope:** PScribo *does* support an Out-<Format>Document plugin convention, so a native -Format Excel plugin would be possible **if** PScribo were ever forked into the AsBuiltReport org. That fork is a long-term consideration only — it requires significant changes across all report repos plus ongoing maintenance — and is **not pursued here**. Should it ever happen, the MVP's traversal/render logic could be lifted into such a plugin with the per-system split staying in Core; nothing in this plan depends on it.
---
4.6 Recommended MVP scope
Objective: prove the concept end-to-end — New-AsBuiltReport ... -Format Excel produces a valid, openable .xlsx from a real report run, with sections as worksheets and report data in cells, using ImportExcel. Everything beyond "it demonstrably works" is deferred.
In scope (minimum to prove functionality):
#
Item
Notes
1
Excel added to the -Format ValidateSet, wired into New-AsBuiltReport
§5.2
2
Core-resident Export-AbrExcelDocument rendering with ImportExcel
§5.1
3
H2 → worksheet; H3–H6 → in-sheet banners with descendant tables collected in document order and stacked vertically
§4.2
4
Standard and List table layouts rendered; \*\_\_Style helper columns dropped from data
keyed-list can be rendered as plain rows for MVP
5
One workbook per system (default), single-system → one file; multi-system → <FileName> - <System>.xlsx
§4.1a
6
Worksheet-name and filename sanitisation + de-duplication
31-char/illegal-char rules
7
Header row styling, freeze top row, auto-size columns
basic readability
8
Soft ImportExcel dependency — load on demand, friendly error if absent when -Format Excel is used
avoids forcing the module on Word-only users
9
One smoke test proving a workbook is generated and re-readable
see validation target below
Explicitly out of scope for the MVP (deferred to later phases per §7):
Health-check cell fill colours from \_\_Style metadata (Phase 2) — the metadata read is proven feasible (§3.3) but not required to demonstrate export.
HardRequiredModules dependency / version pinning (decision deferred — §8.2).
Full localization of new strings across all locales — MVP may ship en-US only and log the rest as follow-up debt.
Configurable WorksheetHeadingLevel UI — MVP can hard-default to 2 and expose the knob later.
Acceptance criteria (definition of "proven"):
New-AsBuiltReport -Report <X> -Target <t> -Format Excel writes a .xlsx that opens cleanly in Excel with no repair prompt.
One worksheet per H2, tab names correct/sanitised/unique.
Table headers and row values match the equivalent Word/HTML output for the same run.
A multi-target run produces one file per system (default mode).
-Format Word,Excel works in one run; no regression to Word/HTML/Text.
With ImportExcel absent, -Format Excel fails with a clear, actionable message (not a crash).
Suggested validation target: AsBuiltReport.System.Resources. This is the best MVP report module because it reports on the local machine — no remote infrastructure, no lab, no real credentials — so it runs fast and repeatably on the developer's own box. Verified structure (Invoke-AsBuiltReport.System.Resources.ps1 + Src/Private):
foreach ($System in $Target) { Section -Style Heading1 "$($System.ToUpper())" { ... } } — the standard one H1 per system pattern, so it also exercises the per-system file split with multiple targets.
Exactly 5 Heading2 sections — Date, Process Info, PowerShell Host, Time Zone, Uptime — each emitting tables → a predictable, small 5-worksheet workbook that's trivial to eyeball and assert against.
No Invoke-Command / CimSession / PSSession / -ComputerName usage — purely local data gathering.
Validate in two layers:
Automated (deterministic, CI-safe): build a small synthetic PScribo.Document fixture in the unit test — one Document → 1–2 H1s → a few H2s, each with a standard table, a list table, and a nested H3 table including \_\_Style cells — then run Export-AbrExcelDocument and re-open the result with Import-Excel/Open-ExcelPackage to assert worksheet count, tab names, headers, and values. Needs no live target (§5.6).
End-to-end: run AsBuiltReport.System.Resources against localhost (single system) and against two hostnames (multi-system) to confirm a real document renders, splits into per-system files, and opens cleanly in Excel.
Effort: ~2–3 days (= Phase 1 in §7).
5. Implementation plan (Core-resident, ImportExcel)
functionExport-AbrExcelDocument {
\[CmdletBinding()]
param (
\[Parameter(Mandatory)] \[System.Management.Automation.PSObject] $Document,# the PScribo.Document
\[Parameter(Mandatory)] \[string] $Path,
\[string] $FileName,
\[int] $WorksheetHeadingLevel=2,
\[switch] $IncludeParagraphs
)
# 1. Find all Heading1 sections (Level 0) = one per system. (Skip cover/TOC/title nodes.)# 2. FOR EACH H1 (system):# a. Compute the per-system output filename:# single H1 -> "$FileName.xlsx"# multiple -> "$FileName - <sanitised H1 name>.xlsx" (dedupe collisions)# b. Resolve worksheet sections WITHIN this H1: recurse its .Sections collecting# nodes where Level -eq $WorksheetHeadingLevel (i.e. H2 by default).# c. For each worksheet section, recursively gather descendant PScribo.Table nodes# (with owning sub-section name/number for banding).# d. For each table: project Rows minus \*\_\_Style cols -> Export-Excel# -WorksheetName <sanitised> -PassThru; apply header style, freeze top row,# autosize, table style; stack multiple tables vertically with banded sub-headers.# e. Walk \*\_\_Style metadata -> Set-ExcelRange -BackgroundColor (mapped hex) per cell/row.# f. Prepend a "Report Info" sheet (Author/Company/Version/Date for this system) and# an "Index" sheet with hyperlinks to each tab.# g. Close-ExcelPackage.# 3. Return one \[FileInfo] PER system (the caller collects them all).
}
The function returns an array of FileInfo — one per system — so the entry point can report and (optionally) email every generated workbook.
Helpers (private): ConvertTo-AbrExcelWorksheetName (sanitise/dedupe), Get-AbrExcelStyleColor (style name → hex from the Style.ps1 palette), Get-AbrPScriboTableDescendant (recursive table collector).
After the existing Export-Document call (~line 622), branch: PScribo handles Word/HTML/Text; if Excel is in $Format, call Export-AbrExcelDocument -Document $AsBuiltReport -Path $OutputFolderPath -FileName $FileName, which returns one file per system. Append all returned files to $Document so the existing -SendEmail path attaches every per-system workbook, and emit one OutputFolder success line per file.
Keep the original $Format minus Excel for the PScribo call so PScribo never receives an unknown format.
5.3 Declare the dependency — soft / load-on-demand (decided for MVP)
ImportExcel is a soft dependency for the MVP: it is not added to AsBuiltReport.Core.psd1's RequiredModules, so users who never produce Excel are unaffected. Instead, it is loaded only when needed:
In New-AsBuiltReport (or at the top of Export-AbrExcelDocument), when Excel is in $Format, attempt to load it and fail clearly if absent:
if ($Format-contains'Excel') {
if (-not (Get-Module-ListAvailable -Name ImportExcel)) {
Write-Error"The 'Excel' output format requires the ImportExcel module. Install it with: Install-Module ImportExcel -Scope CurrentUser"-ErrorAction Stop
}
Import-Module ImportExcel -ErrorAction Stop
}
Mention ImportExcel in the README/user-guide as an optional module required only for -Format Excel.
**Deferred to release (not MVP):** whether to promote ImportExcel to a hard RequiredModules entry (with a pinned minimum version, e.g. 7.x) once Excel is a first-class supported format. Revisit at GA — see §8.2.
5.4 Module loading
AsBuiltReport.Core.psm1 already dot-sources everything in Src/Private/, so the new function loads automatically. No psm1 change unless we choose to promote it to global scope (not needed — it's only called from Core).
5.5 Localization
Add strings (export progress, "Excel module not found", sheet-name truncation warning) to all locale files under Language/\*/New-AsBuiltReport.psd1 (minimum en-US), per the project's localization rule.
5.6 Tests
Tests/Unit/Export-AbrExcelDocument.Tests.ps1: build a small synthetic PScribo.Document (Document → ×ばつH1 → ×ばつH2, each with a table incl. \_\_Style cells), run the exporter, then re-open with Import-Excel/Open-ExcelPackage and assert: worksheet count = H2 count, tab names sanitised/unique, header row present, data values intact, \_\_Style cells got the expected fill.
Tests/Unit/New-AsBuiltReport.Tests.ps1: assert Excel is now a valid -Format value.
Tests/Quality: ensure new file is UTF-8, has comment-based help, passes PSScriptAnalyzer, uses approved verbs (Export- ✓).
Gate Excel tests on ImportExcel being available (skip with message otherwise) so CI without the module still passes.
5.7 Docs & changelog
CHANGELOG.md: ### Added — Excel (XLSX) export via -Format Excel (ImportExcel). Branch off dev; PR targets dev.
README / user-guide: new format, the WorksheetHeadingLevel knob, and the "what doesn't export" note (images/prose).
Multi-system run:... -Target vcenter01,vcenter02 -Format Word,Excel produces one combined Word doc (both vCenters as chapters) but two workbooks:
VMware vSphere As Built Report - vcenter01.xlsx
VMware vSphere As Built Report - vcenter02.xlsx
each containing only that vCenter's worksheet tabs.
Datastores tab: top band = all-datastores summary table; below it, one banded block per datastore (detail list table + any "SCSI LUN Info" table), in document order.
Cells where datastore usage ≥ 90% are filled FEDDD7 (Critical), 75–90% FFF4C7 (Warning) — identical thresholds to the Word health check, read from the \_\_Style metadata.
---
7. Effort & phasing
Phase
Work
Est.
1 — MVP
-Format Excel, one workbook per Heading1/system, H2→worksheet (level configurable), standard + list tables, header styling, freeze/autosize, sheet-name + filename sanitisation, Report Info sheet, manifest dependency
2–3 days
2 — Health checks
Read \_\_Style metadata → cell/row fills + Index sheet with hyperlinks
Pester unit tests, quality tests, changelog, README/user-guide, localization strings
1–2 days
(Optional later)
Refactor into upstream PScribo Out-ExcelDocument plugin
separate
Total MVP→shippable: ~1 working week.
---
8. Risks, edge cases & decisions for you
H1-vs-H2 mapping — ✅ DECIDED: Heading2 → worksheet. Implemented as Option C (configurable WorksheetHeadingLevel) with the default set to level 2, so out-of-the-box each H2 becomes its own tab and H1 (the target name) is carried as the title / "Report Info" sheet.
Hard vs. soft ImportExcel dependency — ✅ DECIDED for MVP: soft / load-on-demand. ImportExcel is loaded only when -Format Excel is requested, with a clear actionable error if missing; it is not added to RequiredModules, so Word-only users are unaffected (§5.3). Promotion to a hard, version-pinned RequiredModules entry is deferred to GA.
Duplicate / >31-char tab names — handled by central sanitiser; long names truncated with a logged warning.
3a. Per-system filenames — system (H1) names used in <FileName> - <System>.xlsx must be stripped of invalid path chars (\\ / : \* ? " < > |) and de-duplicated on collision; guard against over-long paths. Single-system runs keep the plain filename.
Wide tables — many report tables have 8–15+ columns; fine in Excel (auto-size + freeze), better than Word in fact.
Array-valued cells (e.g. tags joined with newlines) — flatten to a single string per cell, mirroring what the Text plugin does.
InfoLevel variance — the workbook's tab set depends on the report config JSON; that's expected and self-consistent (we export whatever sections were rendered). No pre-introspection needed since we read the built object.
Images/diagrams not exported — documented limitation; Word/HTML remain the "rich" formats. Excel is the data/analysis format.
PScribo unmaintained — PScribo is no longer actively maintained (a possible long-term org fork is noted in §4.5 but explicitly out of scope). This poses no risk to the Excel feature: the Core-resident exporter only reads stable, long-standing public properties (.Sections, .Level, .Rows, .Columns, .IsList, \*\_\_Style) of the already-built document object, and adds nothing to PScribo itself.
---
9. Bottom line
Feasible and clean — the in-memory PScribo document gives us structured sections, object-shaped table rows, and per-cell health-check metadata, all of which ImportExcel consumes naturally.
Self-contained — implement as a Core private function (Export-AbrExcelDocument) invoked from New-AsBuiltReport; zero changes to any report module and no fork of PScribo.
One design correction — map Heading2 → worksheet (configurable, default), because reports have only one Heading1. Confirm this and the dependency model (§8.1–8.2) and Phase 1 can begin.
It's not yet fully announced on PowerShell community but PSWriteOffice can do Word, Excel, PowerPoint, PDF, Markdown or even ODF/RTF/HTML/CSV/Visio and all other things you can imagine doing. It's also materially faster than ImportExcel and a lot more functional.
I'm still finalizing the shape of pswriteoffice, and doing some changes but effectively one module to rule them all. I'm open on feedback on pswriteoffice shape if something is missing. OfficeIMO is 96 projects or so, and PSWriteOffice can use all that.
Maybe this will solve your dependency problem and will give you Word development and huge featureset with ability to save as pdf, markdown and excel, csv etc
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji