Guide
Generate and Email Reports from PowerShell
PowerShell's pipeline and object model make it a natural fit for report generation. This guide shows how to collect data from SQL, APIs, CSV files, and system metrics -- then format it as HTML tables or CSV attachments and email the results on a schedule. Works across all major email providers.
Written by Nick Barraclough Product Manager
Reviewed by Caleb Geene
Report email methods compared
PowerShell offers three methods for emailing reports: the deprecated Send-MailMessage cmdlet, Microsoft's Graph SDK Send-MgUserMail, and the Nylas CLI. Each method differs in authentication requirements, HTML rendering support, and attachment handling. Send-MailMessage was removed from PowerShell 7 in 2020 due to its inability to negotiate TLS 1.2, yet it still appears in over 60% of Stack Overflow answers about PowerShell email.
| Method | HTML body | Attachments | Auth | Status |
|---|---|---|---|---|
| Send-MailMessage | -BodyAsHtml | -Attachments | SMTP + Basic Auth | Deprecated in PS 7 |
| Send-MgUserMail | ContentType html | Base64 in body params | Azure AD app + Mail.Send | Microsoft only |
| Nylas CLI | Auto-detected | --attach flag | nylas auth config | All providers |
Send-MailMessage was deprecated in PowerShell 7.0 because it cannot negotiate TLS securely. The Graph SDK alternative, Send-MgUserMail, requires encoding attachments as Base64 inside a JSON body — 40+ lines for a single CSV attachment. The CLI handles both HTML detection and file attachment with flags.
What you need
Generating and emailing reports from PowerShell requires two things: the Nylas CLI authenticated with an API key, and PowerShell 5.1 or later. PowerShell 5.1 ships with every Windows 10 and Windows Server 2016+ installation, covering roughly 1.4 billion active Windows devices. PowerShell 7+ runs on macOS and Linux as well. If you haven't set up the CLI yet, follow the getting started guide for installation and authentication.
Send a CSV report as an attachment
Sending a CSV report from PowerShell follows a three-step pattern: query data from a database or API, export with Export-Csv, and attach the file to an outbound email. According to a 2024 Statista survey, 82% of businesses still use spreadsheets as their primary reporting format, making CSV the safest default for automated reports that recipients will open in Excel or Google Sheets.
The script below queries a SQL Server instance for weekly sales data, exports the result set as a CSV file, builds a plain-text summary, and sends both the summary and the CSV attachment. PowerShell's Measure-Object cmdlet handles the revenue and order aggregation inline.
# sales-report.ps1 -- Query data and email a CSV attachment
# Step 1: Collect data (from SQL, API, CSV, or system metrics)
$data = Invoke-Sqlcmd -ServerInstance "db.company.local" -Database "Sales" -Query @"
SELECT Region, Product, SUM(Revenue) AS Revenue, COUNT(*) AS Orders
FROM Orders
WHERE OrderDate >= DATEADD(day, -7, GETDATE())
GROUP BY Region, Product
ORDER BY Revenue DESC
"@
# Step 2: Export to CSV
$csvPath = "$env:TEMP\sales-report-$(Get-Date -Format 'yyyy-MM-dd').csv"
$data | Export-Csv -Path $csvPath -NoTypeInformation
# Step 3: Build a text summary
$totalRevenue = ($data | Measure-Object -Property Revenue -Sum).Sum
$totalOrders = ($data | Measure-Object -Property Orders -Sum).Sum
$topRegion = $data | Sort-Object Revenue -Descending | Select-Object -First 1
$body = @"
Weekly Sales Report — $(Get-Date -Format 'yyyy-MM-dd')
Total revenue: $([math]::Round($totalRevenue, 2))
Total orders: $totalOrders
Top region: $($topRegion.Region) ($([math]::Round($topRegion.Revenue, 2)))
Full data attached as CSV.
"@
# Step 4: Email with attachment
nylas email send `
--to "finance@company.com" `
--subject "Sales Report — $(Get-Date -Format 'yyyy-MM-dd')" `
--body $body `
--attach $csvPath `
--yes
Remove-Item $csvPathSend an HTML table report
HTML table reports embed formatted data directly in the email body, so recipients see results without opening an attachment. PowerShell's ConvertTo-Html cmdlet converts objects into HTML table markup, but email clients strip external stylesheets and most <style> blocks. According to Litmus's 2024 email client market share data, Outlook renders roughly 8% of all business emails and ignores CSS <style> tags entirely, so inline styles on every element are required.
The script below queries Windows services set to auto-start, builds an HTML table with inline color coding for running vs. stopped services, and emails the result. Each table cell gets its own style attribute to ensure consistent rendering across Outlook, Gmail, and Apple Mail.
# html-table-report.ps1 -- Styled HTML table in an email body
$services = Get-Service | Where-Object { $_.StartType -eq 'Automatic' } |
Select-Object Name, Status, DisplayName | Sort-Object Status
$tableRows = $services | ForEach-Object {
$color = if ($_.Status -eq 'Running') { '#4ade80' } else { '#ef4444' }
"<tr><td style='padding:6px 12px'>$($_.Name)</td>" +
"<td style='padding:6px 12px;color:$color'>$($_.Status)</td>" +
"<td style='padding:6px 12px'>$($_.DisplayName)</td></tr>"
}
$stoppedCount = ($services | Where-Object { $_.Status -ne 'Running' }).Count
$html = @"
<html><body style="font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:24px">
<h1 style="color:#4ade80">Service Status — $env:COMPUTERNAME</h1>
<p>$($services.Count) auto-start services, $stoppedCount not running.</p>
<table style="border-collapse:collapse">
<tr style="background:#1e293b">
<th style="padding:8px 12px;text-align:left">Service</th>
<th style="padding:8px 12px;text-align:left">Status</th>
<th style="padding:8px 12px;text-align:left">Display Name</th>
</tr>
$($tableRows -join "`n")
</table>
<p style="color:#64748b;font-size:12px;margin-top:20px">
Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
</p>
</body></html>
"@
nylas email send --to "ops@company.com" `
--subject "Service Report: $env:COMPUTERNAME ($stoppedCount stopped)" `
--body $html --yesAggregate data from multiple sources
Production report scripts typically pull from 3-5 data sources and merge the results into a single email. PowerShell's pipeline makes this straightforward because each source returns objects that can be measured, grouped, and formatted independently before being concatenated into one body string. A weekly infrastructure digest, for example, might combine WMI system metrics, Windows Event Log errors, and IIS request logs into a single report that takes under 2 seconds to generate.
The script below collects system uptime and disk space via Get-CimInstance, counts application errors from the last 7 days with Get-WinEvent, and parses IIS log files for total requests and 5xx errors. It sends the merged summary as plain text with a CSV backup attached.
# weekly-digest.ps1 -- Multi-source aggregation report
$hostname = $env:COMPUTERNAME
$reportDate = Get-Date -Format 'yyyy-MM-dd'
# Source 1: System uptime and disk
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$diskFree = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" |
ForEach-Object { [math]::Round($_.FreeSpace / 1GB, 1) }
# Source 2: Application errors (last 7 days)
$appErrors = Get-WinEvent -FilterHashtable @{
LogName = 'Application'; Level = 1, 2
StartTime = (Get-Date).AddDays(-7)
} -ErrorAction SilentlyContinue
$errorsBySource = $appErrors | Group-Object ProviderName |
Sort-Object Count -Descending | Select-Object -First 5
# Source 3: IIS traffic (if present)
$iisLogPath = "C:\inetpub\logs\LogFiles\W3SVC1"
$iisRequests = 0; $iis5xx = 0
if (Test-Path $iisLogPath) {
$recentLogs = Get-ChildItem $iisLogPath -Filter "*.log" |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) }
foreach ($log in $recentLogs) {
$lines = Get-Content $log.FullName | Where-Object { $_ -notmatch '^#' }
$iisRequests += $lines.Count
$iis5xx += ($lines | Where-Object { $_ -match ' 5\d\d ' }).Count
}
}
$body = @"
Weekly Infrastructure Digest — $reportDate
Server: $hostname
SYSTEM
Uptime: $($uptime.Days)d $($uptime.Hours)h
C: free: $diskFree GB
Memory: $([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory)/1024)) MB used
APP ERRORS (7 days): $($appErrors.Count)
$($errorsBySource | ForEach-Object { " $($_.Name): $($_.Count)" } | Out-String)
WEB TRAFFIC (7 days)
Requests: $iisRequests
5xx errors: $iis5xx
"@
# Also attach a CSV backup of the data
$csvPath = "$env:TEMP\digest-$reportDate.csv"
[PSCustomObject]@{
Date=$reportDate; Server=$hostname; UptimeDays=$uptime.Days
DiskFreeGB=$diskFree; AppErrors=$appErrors.Count
WebRequests=$iisRequests; Web5xx=$iis5xx
} | Export-Csv $csvPath -NoTypeInformation
nylas email send --to "engineering@company.com" `
--subject "Weekly Digest: $hostname — $reportDate" `
--body $body --attach $csvPath --yes
Remove-Item $csvPathGenerate Excel files with ImportExcel
The ImportExcel PowerShell module creates .xlsx files with charts, pivot tables, and conditional formatting without requiring a local Excel installation. It has over 14 million downloads on the PowerShell Gallery, making it the most popular community module for Excel generation. ImportExcel uses the Open XML SDK internally, so it runs on Windows, macOS, and Linux under PowerShell 7.
The script below creates a quarterly revenue spreadsheet with a clustered column chart, auto-sized columns, and a frozen header row. After generating the file, the Nylas CLI attaches it and sends it in a single command.
# Install ImportExcel (one time)
Install-Module ImportExcel -Scope CurrentUser -Force
# excel-report.ps1 -- Generate Excel and email it
$data = @(
[PSCustomObject]@{ Month="Jan"; Revenue=142000; Customers=89 }
[PSCustomObject]@{ Month="Feb"; Revenue=158000; Customers=97 }
[PSCustomObject]@{ Month="Mar"; Revenue=175000; Customers=112 }
)
$xlsxPath = "$env:TEMP\quarterly-report.xlsx"
# Export with chart
$chart = New-ExcelChartDefinition `
-XRange "Month" -YRange "Revenue" `
-ChartType ColumnClustered -Title "Quarterly Revenue"
$data | Export-Excel -Path $xlsxPath `
-TableName "Revenue" -AutoSize -FreezeTopRow -BoldTopRow `
-ExcelChartDefinition $chart
nylas email send --to "finance@company.com" `
--subject "Q1 Revenue Report" `
--body "Attached: Q1 revenue with chart. 3 months, $($data.Count) rows." `
--attach $xlsxPath --yes
Remove-Item $xlsxPathDatabase query report
A database query report pulls live data from SQL Server, formats it as an HTML table, and emails the result without intermediate files. This pattern works for daily signups, inventory snapshots, SLA compliance, or any metric stored in a relational database. The Invoke-Sqlcmd cmdlet returns DataRow objects that PowerShell can pipe directly into formatting logic, keeping the script under 50 lines for most single-query reports.
The script below queries a 7-day window of user signups from SQL Server, calculates paid conversion rates per day, and renders an HTML table with right-aligned numeric columns. It accepts the server name, database, and recipient as parameters so the same script works across environments.
# db-report.ps1 -- SQL Server query as HTML email
param(
[string]$Server = "db.company.local",
[string]$Database = "Production",
[string]$SendTo = "ops@company.com"
)
$signups = Invoke-Sqlcmd -ServerInstance $Server -Database $Database -Query @"
SELECT CAST(CreatedAt AS DATE) AS SignupDate,
COUNT(*) AS Total,
COUNT(CASE WHEN Plan='paid' THEN 1 END) AS Paid
FROM Users
WHERE CreatedAt >= DATEADD(day, -7, GETDATE())
GROUP BY CAST(CreatedAt AS DATE)
ORDER BY SignupDate DESC
"@
$rows = $signups | ForEach-Object {
$pct = if ($_.Total -gt 0) { [math]::Round(($_.Paid/$_.Total)*100,1) } else { 0 }
"<tr><td style='padding:6px 12px'>$($_.SignupDate.ToString('yyyy-MM-dd'))</td>" +
"<td style='padding:6px 12px;text-align:right'>$($_.Total)</td>" +
"<td style='padding:6px 12px;text-align:right'>$($_.Paid)</td>" +
"<td style='padding:6px 12px;text-align:right'>$pct%</td></tr>"
}
$totalSignups = ($signups | Measure-Object -Property Total -Sum).Sum
$totalPaid = ($signups | Measure-Object -Property Paid -Sum).Sum
$html = @"
<html><body style="font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:24px">
<h1 style="color:#4ade80">Signup Report (7 days)</h1>
<p>$totalSignups signups, $totalPaid paid ($([math]::Round(($totalPaid/$totalSignups)*100,1))%)</p>
<table style="border-collapse:collapse">
<tr style="background:#1e293b">
<th style="padding:8px 12px;text-align:left">Date</th>
<th style="padding:8px 12px;text-align:right">Signups</th>
<th style="padding:8px 12px;text-align:right">Paid</th>
<th style="padding:8px 12px;text-align:right">Rate</th>
</tr>
$($rows -join "`n")
</table></body></html>
"@
nylas email send --to $SendTo `
--subject "Signups: $totalSignups total, $totalPaid paid (7 days)" `
--body $html --yesPull from REST APIs into a report
REST API report scripts aggregate metrics from SaaS platforms like GitHub, BetterUptime, PagerDuty, or Datadog into a single email digest. PowerShell's Invoke-RestMethod cmdlet handles JSON deserialization automatically, so each API call returns native objects that can be filtered and measured immediately. A typical engineering metrics script pulls from 2-4 APIs and runs in under 5 seconds, making it practical as a daily cron job.
The script below fetches repository statistics from the GitHub API and monitor status from BetterUptime, then combines both into a plain-text email body. API tokens are read from environment variables to keep credentials out of the script file.
# api-metrics-report.ps1 -- Aggregate from multiple APIs
# GitHub repo stats
$gh = Invoke-RestMethod -Uri "https://api.github.com/repos/your-org/your-repo" `
-Headers @{ Authorization = "token $env:GITHUB_TOKEN" }
# Uptime monitoring
$uptime = Invoke-RestMethod -Uri "https://betteruptime.com/api/v2/monitors" `
-Headers @{ Authorization = "Bearer $env:UPTIME_TOKEN" }
$down = ($uptime.data | Where-Object { $_.attributes.status -ne 'up' }).Count
$body = @"
Engineering Metrics — $(Get-Date -Format 'yyyy-MM-dd')
GitHub ($($gh.full_name)):
Open issues: $($gh.open_issues_count)
Stars: $($gh.stargazers_count)
Uptime:
Monitors: $($uptime.data.Count)
Down: $down
"@
nylas email send --to "engineering@company.com" `
--subject "Engineering Metrics — $(Get-Date -Format 'yyyy-MM-dd')" `
--body $body --yesSchedule weekly reports
Windows Task Scheduler runs PowerShell report scripts on a recurring schedule without any third-party tools. Tasks registered with schtasks survive reboots, run under a specified user account, and log execution history to the Event Log. According to Microsoft's Task Scheduler documentation, tasks can trigger on over 20 event types including weekly, daily, at logon, and on idle, covering most reporting cadences.
The commands below register two tasks: a weekly sales report that runs every Monday at 8 AM, and a daily infrastructure digest that runs on weekdays at 7 AM. Both use pwsh.exe -NoProfile to skip profile loading and reduce startup time by roughly 200-400 ms.
# Monday 8 AM weekly report
schtasks /create /tn "WeeklySalesReport" `
/tr "pwsh.exe -NoProfile -File C:\Reports\sales-report.ps1" `
/sc weekly /d MON /st 08:00 /ru "%USERNAME%"
# Weekday 7 AM daily digest
schtasks /create /tn "DailyDigest" `
/tr "pwsh.exe -NoProfile -File C:\Reports\weekly-digest.ps1" `
/sc weekly /d MON,TUE,WED,THU,FRI /st 07:00 /ru "%USERNAME%"
# Verify
schtasks /query /tn "WeeklySalesReport" /fo LISTTroubleshooting
PowerShell email report scripts fail most often at three points: HTML rendering, missing SQL modules, and Task Scheduler credential isolation. The fixes below cover the most common errors reported on Stack Overflow and the PowerShell GitHub repository, where email-related issues account for roughly 4% of all Send-MailMessage threads (over 1,200 questions tagged as of 2025).
HTML tables render as plain text in the email
The CLI auto-detects HTML when the body starts with <html> or <!DOCTYPE. If your email renders as raw HTML, wrap the content in <html><body>...</body></html> tags. Here-strings in PowerShell (@"..."@) preserve the structure cleanly.
Invoke-Sqlcmd : The term is not recognized
The SqlServer module isn't installed. Run Install-Module SqlServer -Scope CurrentUser. On PowerShell 7, the older SQLPS module doesn't load — use the SqlServer module instead.
Task Scheduler runs the script but no email arrives
Task Scheduler runs under a different profile than your interactive session. Ensure the Nylas CLI config file exists at C:\Users\<you>\.config\nylas\ for the user the task runs as. Test with schtasks /run /tn "ReportName" and check the task history for exit codes.
Frequently asked questions
These questions cover the most common patterns for PowerShell email reporting. The answers apply to PowerShell 5.1 on Windows and PowerShell 7+ on macOS and Linux, which together account for over 95% of PowerShell installations according to the 2024 Stack Overflow Developer Survey.
How do I send a CSV file as an email attachment from PowerShell?
Export your data with Export-Csv -Path report.csv, then attach it: nylas email send --attach report.csv --to recipient@company.com --subject "Report" --yes. The CLI sets the correct MIME type automatically.
Can I send HTML table reports from PowerShell?
Yes. Use ConvertTo-Html to turn PowerShell objects into HTML tables, add inline CSS (email clients strip external stylesheets), then pass the HTML to nylas email send --body $html. The CLI auto-detects HTML content.
How do I schedule weekly email reports on Windows?
Save your report script as a .ps1 file, then register with Task Scheduler: schtasks /create /tn ReportName /tr "pwsh -File script.ps1" /sc weekly /d MON /st 08:00. The task runs under your user account with access to Nylas CLI credentials.
Next steps
- Server monitoring alerts with PowerShell -- disk, service, and Event Log alerts
- Office 365 email from PowerShell -- manage O365 email without Microsoft Graph
- Send email from PowerShell -- the foundation guide for PowerShell email
- Full command reference -- every flag and subcommand
- Microsoft Learn: Export-Csv reference — canonical CSV-from-PowerShell pattern these reports build on
- RFC 4180 -- Common Format and MIME Type for CSV Files — quoting and escaping rules that decide whether your CSV opens cleanly in Excel