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 with Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP.

By Nick Barraclough

Setup

Install Nylas CLI and authenticate once. See the PowerShell email guide for full install options.

irm https://cli.nylas.com/install.ps1 | iex
nylas auth config  # paste your API key

Send a CSV report as an attachment

The simplest reporting pattern: query data, export to CSV, attach it to an email. According to a 2024 Statista survey, 82% of businesses still use spreadsheets as their primary reporting tool. CSV is universal.

# 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 $csvPath

Send an HTML table report

PowerShell's ConvertTo-Html cmdlet turns objects into HTML tables. Add inline CSS for email-safe styling (email clients strip external stylesheets).

# 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 --yes

Aggregate data from multiple sources

Real reports pull from more than one place. This script combines system metrics, Event Log errors, and IIS traffic stats into a single weekly digest.

# 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 $csvPath

Generate Excel files with ImportExcel

The ImportExcel module creates .xlsx files without needing Excel installed. It has over 14 million downloads on the PowerShell Gallery.

# 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 $xlsxPath

Database query report

Pull data from SQL Server, format as HTML, and email. This pattern works for daily signups, inventory counts, or any metric you track in a database.

# 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 --yes

Pull from REST APIs into a report

Many business metrics live in SaaS tools. Pull them via API and combine into a single email.

# 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 --yes

Schedule weekly reports

# 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 LIST

Frequently asked questions

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