Veeam Client Backup Report — Frequency & 21-Night Running State
| Field | Details |
|---|---|
| Category | Backup & Disaster Recovery — Client Reporting |
| Author | Security & Compliance Engineering (Scott Leister) |
| Date | November 2026 |
| Version | 1.0 |
| Audience | T3 / Engineering (report production); Account managers (delivery) |
| Platform | Veeam Backup & Replication 13.x on DTC BDR appliances, NinjaOne, PowerShell 7.6+, Python 3 |
1. Purpose
This article explains how to produce a client Veeam backup report covering two questions clients ask:
- How often is each server backed up? (configured schedule, observed pattern, retention, offsite copy)
- How have backups run over the last three weeks? (a per-night running state for each server, local and offsite, with health ratings and findings)
It works for single-site and multi-site clients, and was first produced for Smile Loft (9 sites) in November 2026. It also documents how to automate the whole process in Windmill, and includes a skill specification so an AI step can reproduce the analysis consistently.
What this document is NOT: the daily backup monitoring procedure (see Veeam Backup Daily Operations & Verification SOP) or the troubleshooting playbook.
Related Documents
| Document | When to Reference |
|---|---|
| Veeam Backup Daily Operations & Verification SOP | Daily alert triage and weekly reviews |
| Veeam Backup and Replication Standards | How jobs are supposed to be configured (schedules, retention, S3 copy) |
| Veeam BDR Script Toolkit | Other NinjaOne scripts for BDRs |
| NinjaOne Veeam Alerts & Custom Fields — Where to Start | The veeam-inventory script and its custom fields |
| Veeam Version Standard — Pinned Build 13.1.1.18 | Why PowerShell 7 is required on BDRs |
2. Quick Reference
CLIENT REQUESTS A BACKUP REPORT
│
├─ 1. Run "Veeam Backup Schedule and 21 Run History" in NinjaOne
│ against every BDR (DTCBSURE-xxxx) in the client's organization
│
├─ 2. Collect <Site>_<BDR>_schedule.csv and <Site>_<BDR>_history.csv
│ from C:\ProgramData\DTC\VeeamReport on each BDR
│
├─ 3. Run build_veeam_report_data.py → frequency, nightly, summary datasets
│
├─ 4. Review: write findings + recommendations (human or AI draft + human review)
│ ⚠ Verify retention and anything that looks odd against the console
│
└─ 5. Produce the PDF + client email → attach to the HaloPSA ticket → send
Time: about 30 minutes manually for a 9-site client; under 5 minutes of human review once automated in Windmill.
3. Report Contents
The finished report (PDF, landscape letter) has five sections:
| Section | What it shows |
|---|---|
| Summary | Protected servers, % of scheduled local backup nights completed, count of Critical and Attention streams |
| Findings and recommendations | Prioritized issues (Critical, High, Medium, Info) in plain language with a recommended action |
| Backup frequency by server | Per server: backup method, Veeam job, configured schedule, observed pattern, completed runs, typical start, restore points, offsite copy yes/no |
| Running state, last 21 nights | Grid of every server × night, local and offsite rows, colored Success / Warning / Failed / No run, weekends shaded |
| 21-night summary by server | Per server and copy type: nights by result, missed scheduled nights, failed attempts, last completed run, health rating |
Scope default: servers only. Workstations are produced as a separate report on request (use --all-machines).
4. Prerequisites
- NinjaOne script Veeam Backup Schedule and 21 Run History (
Get-VeeamBackupReportData.ps1, attached to this page) in the script library, set to 64-bit, run as SYSTEM. - PowerShell 7.6+ on each BDR. Veeam B&R 13's module requires it; the script relaunches itself under
pwsh.exeautomatically. If missing:winget install Microsoft.PowerShell. - Python 3 with
pandasandreportlab(on your workstation or a Windmill worker). build_veeam_report_data.py(attached to this page).
5. Procedure
Step 1 — Collect the data from each BDR
- In NinjaOne, open the client organization and select every BDR device (
DTCBSURE-xxxx). - Run Veeam Backup Schedule and 21 Run History. Optional parameters:
-SiteName— friendly site name (defaults to the NinjaOne location name). Use it if location names aren't client-friendly.-Days— look-back window, default21.-OutputPath— defaultC:\ProgramData\DTC\VeeamReport.
- Check each activity result. A good run ends with:
Site: <name> | Server: DTCBSURE-xxxx | Jobs/machines scheduled: N | Runs in last 21 days: N (...)
| Error in the NinjaOne output | Cause | Fix |
|---|---|---|
requires a minimum Windows PowerShell version of '7.6' |
VBR 13 module, script ran in Windows PowerShell 5.1 | Script relaunches under pwsh automatically; if it says pwsh.exe not found, install PowerShell 7 |
No snap-ins have been registered |
Old snap-in fallback; real cause is in the lines above it | Read the "Detected:" line and the module errors |
Standalone Veeam Agent for Windows only |
Device is not a VBR server | Run it on the BDR instead |
32-bit host detected; relaunching |
Script set to 32-bit in NinjaOne | Informational; set the script architecture to 64-bit |
Step 2 — Gather the CSVs
Each BDR writes two files to C:\ProgramData\DTC\VeeamReport:
| File | Contents |
|---|---|
<Site>_<BDR>_schedule.csv |
One row per job/protected object: job type, schedule kind and detail, next run, restore points, enabled |
<Site>_<BDR>_history.csv |
One row per machine per session in the window: job, start, end, result, duration, retry flag, GB transferred, reason |
Pull them with NinjaOne remote file transfer or the remote shell and put them all in one folder. (In Windmill, the script uploads them directly — see section 8.)
Step 3 — Build the datasets
python build_veeam_report_data.py <csv_folder> <output_folder> --end <last night, YYYY-MM-DD> --site-prefix "<Client Name> "
Use --all-machines to include workstations. Output:
| File | Use |
|---|---|
backup_frequency.csv |
Backup frequency table |
nightly_status.csv |
21-night grid |
machine_summary.csv |
Summary table and health ratings |
The script prints the health counts, for example 15 machines, 30 streams: {'Healthy': 18, 'Attention': 11, 'Critical': 1}.
Step 4 — Review and write findings
Work through machine_summary.csv from Critical down, then scan the grid for patterns. Write each finding as: Priority · Site · one-line finding · plain-language detail · recommendation. Use these patterns:
| What you see | What it usually means | Typical recommendation |
|---|---|---|
Every local run ends in Warning with SQL VSS Writer is missing |
SQL databases captured crash-consistent; no log processing | Repair/re-register the SQL Server VSS Writer |
| Offsite rows Failed for many nights, local rows green | S3 copy job broken; cloud copy is stale | Confirm the offsite chain is intact; fix the copy job |
Offsite Warning Backup copy RPO violation ... not copied within 1 day |
Copy is running late or scheduled before the backup | Check the copy schedule (see 8 PM vs 9 PM gotcha) and bandwidth |
Failed at Sending backup job configuration on recurring nights |
Agent unreachable — machine off or asleep at 9 PM | Keep machine on overnight / Wake-on-LAN |
Offsite Restore point is incomplete once |
Local run failed the night before | No action if the next runs are clean |
| No local sessions for a few days while offsite continued | Job edited or recreated, or machine added to the job late | Check the job's history in the console |
A server protected by the Workstations agent policy |
Gets workstation settings | Move to a server job |
| Schedule says Disabled | Nothing is being backed up | Confirm scope; re-enable |
Always verify before sending:
- Retention. The export's
RetainRestorePointsvalue is ambiguous on VBR 13 (it returned7everywhere). DTC's standard is about 2 weeks of restore points locally and 30 days in the cloud. Confirm the actual setting in the console for at least one job per site; do not put a retention claim in the report that you haven't checked. - Anything that looks hourly on an agent server. Confirm against the console (see gotchas).
- Keep finding wording factual — the findings are an interpretation of Veeam status messages.
Step 5 — Produce and deliver
- Generate the PDF (landscape letter, DTC footer with date and page numbers, each site's rows kept together on a page). The Windmill flow does this automatically; manually, use Claude with this page's skill specification and the three CSVs plus your findings.
- Draft the client email: lead with the overall result (% of nights completed, every site has a recent backup), then each issue in plain language with what DTC is doing about it, then retention in plain terms, then an offer to walk through it. No internal jargon; spell out acronyms on first use.
- Attach the PDF and the email to the HaloPSA ticket and send from the ticket.
6. How the Analysis Works (the rules)
These rules are what make reports consistent between runs and between people. The analysis script implements them; an AI step must follow them exactly.
| Rule | Definition |
|---|---|
| Backup night | 8:00 AM to 8:00 AM the next day, labeled by the start date. A 9 PM job and its retries after midnight count toward the same night. |
| Tier | Sessions from jobs named S3 Copy... are Offsite; everything else is Local. |
| Night status | The best result of any session for that machine, tier and night: Success > Warning > Failed > No run. |
| Machine name | Upper-cased. Job-level copy rows take the machine from the job name (... - <machine>). Short copy-job aliases (MR-PANO) map to the local name (SLMR-PANO). |
| Role | Anything in a Hyper-V host-level job is a Server (VM). Agent machines are Servers if the name contains server, srv, dc, data, remote, rds, ts, softdent, fileserver or hvhost; otherwise Workstation. Review the result. |
| Agent check-ins | On VBR 13, agent sessions shorter than 60 seconds are management check-ins, not backups. Exclude them from run counts and the observed pattern (they still count toward the night's status). |
| Expected nights | From the configured schedule: Mon–Fri by default, Mon–Sat or every day where configured, every day for hourly jobs. |
| Missed night | An expected night with no session at all. |
| Health — Critical | No completed (Success/Warning) run in the window, OR nothing completed in the last 2 days, OR Failed + Missed nights ≥ 5. |
| Health — Attention | At least one Failed, Warning or Missed night. |
| Health — Healthy | Every expected night completed with Success. |
| Local completion % | (Success + Warning nights) ÷ (Success + Warning + Failed + Missed nights), Local tier only. |
7. Known Limitations and Gotchas
- VBR 13 requires PowerShell 7.6. NinjaOne runs 5.1. The script relaunches under
pwsh.exe. - Agent sessions have a blank job name in the history export. They're identified as agent sessions by that blank.
- Agent check-ins look like hourly backups. At Suitland, SDCSRV showed 522 "successful runs" — they were 2-second check-ins; the real backups ran nightly Mon–Sat at 9 PM. The 60-second rule fixes this.
- "Failed attempts" overcounts on some workstations for the same reason (thousands of short failed check-ins). Use failed nights, not attempts, for findings.
- Copy job child sessions are named
<copy job>\<source job> - <machine>. Anything matching job names exactly (including the veeam-inventory script) must match on the part before\. - Offsite copy job schedules aren't in the export (copy jobs are excluded from the schedule pass). Offsite frequency is shown as observed.
- Copy scheduled before the backup. A daily S3 copy at 8 PM with a 9 PM backup means the cloud is always about 23 hours behind and throws RPO warnings. Schedule copies after the backup window.
- Retention values need console confirmation (see Step 4).
8. Automating in Windmill
8.1 Architecture
Windmill schedule or manual run (inputs: NinjaOne org ID, client name, nights=21, servers_only=true)
│
├─ Step 1 ninja_get_bdrs (Python) NinjaOne API → devices in org named DTCBSURE-*
├─ Step 2 ninja_run_collection (Python) NinjaOne API → run the collection script on each BDR,
│ passing an upload URL and a one-time token
├─ Step 3 wait_for_uploads (Python) poll Windmill object storage until every BDR has
│ uploaded both CSVs (timeout 30 min; list missing BDRs)
├─ Step 4 build_datasets (Python) build_veeam_report_data.py logic → 3 CSVs
├─ Step 5 draft_findings (Python) Claude API with the skill spec (section 9) → findings.csv
│ + client email draft
├─ Step 6 approval (Windmill suspend/approval step) engineer reviews findings,
│ retention statement and email; can edit before resuming
├─ Step 7 render_pdf (Python) reportlab → PDF
└─ Step 8 deliver (Python) HaloPSA API → attach PDF + email draft to the ticket
8.2 Windmill resources and secrets
| Resource | Contents |
|---|---|
ninjaone |
OAuth client ID/secret (client credentials, scope monitoring management), region base URL |
anthropic |
Claude API key |
halopsa |
HaloPSA API client ID/secret and tenant URL |
| Workspace object storage (S3) | Where BDRs upload CSVs and the PDF is written |
8.3 Getting the CSVs from the BDRs to Windmill
NinjaOne script output can't carry 700 KB files, so the collection script uploads them itself. Add two parameters and this block to the end of Get-VeeamBackupReportData.ps1:
param(
# ...existing parameters...
[string]$UploadUrl, # Windmill webhook for the upload script, passed by the flow
[string]$UploadToken # short-lived Windmill token, passed by the flow
)
# ...existing script...
if ($UploadUrl -and $UploadToken) {
foreach ($f in @($schedFile, $histFile)) {
$body = @{
run_id = $env:NINJA_RUN_ID
site = $SiteName
server = $server
filename = Split-Path $f -Leaf
content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($f))
} | ConvertTo-Json
Invoke-RestMethod -Uri $UploadUrl -Method Post -Body $body -ContentType 'application/json' `
-Headers @{ Authorization = "Bearer $UploadToken" } -TimeoutSec 120
}
Write-Output "Uploaded CSVs to report pipeline."
}
The Windmill upload script (Python) decodes content and writes it to object storage under veeam-report/<run_id>/<filename>. Create a token scoped to that one script and expire it when the flow finishes.
8.4 NinjaOne API calls (verify against current NinjaOne API docs)
- Token:
POST https://<region>.ninjarmm.com/ws/oauth/token(client credentials) - Devices in an organization:
GET /v2/organization/{orgId}/devices→ filtersystemNamestartingDTCBSURE - Run a script:
POST /v2/device/{deviceId}/script/runwith{"type": "SCRIPT", "id": <scriptId>, "parameters": "-UploadUrl ... -UploadToken ...", "runAs": "SYSTEM"}
8.5 Operational notes
- Human approval is required before anything reaches a client (Step 6). The AI draft is a starting point.
- Run at most one report per client at a time; the flow's
run_idkeeps uploads separate. - Log the BDRs that didn't upload and say so in the approval step — never silently produce a report missing a site.
- For recurring monthly reports, schedule the flow for the first business day of the month with
nights=21or the full month.
9. Skill Specification (AI step and Claude skill)
Use this as the system prompt for the Windmill draft_findings step, or save it as a Claude skill named veeam-client-backup-report.
NAME: veeam-client-backup-report
WHEN TO USE: A client asks for a Veeam backup report on server backup frequency and recent
running state, for one or more DTC BDR sites.
INPUTS
- backup_frequency.csv, nightly_status.csv, machine_summary.csv (from build_veeam_report_data.py)
- Client name, reporting window, DTC retention standard (about 2 weeks local, 30 days cloud)
unless the engineer confirms otherwise
- Optional: failure reasons from the raw history CSVs
RULES
1. Use only the data provided. Never invent machines, dates, counts or causes.
2. Follow the analysis rules in KB "Veeam Client Backup Report" section 6 exactly
(night window, best result per night, 60-second agent check-in rule, health thresholds).
3. Servers only unless told otherwise.
4. Findings: one per issue, ordered Critical > High > Medium > Info. Each has Priority, Sites,
Finding (one line), Detail (facts and numbers), Recommendation (one action).
- Critical: any Critical-health stream, any server with no completed backup, offsite chain
failing for 5+ nights.
- High: data protected but not consistently (VSS writer missing, application-consistency lost).
- Medium: intermittent failures, RPO warnings, history gaps.
- Info: configuration observations (retention, hourly jobs, naming, scope).
5. Treat these as needing human verification and say so: retention values, any agent server
that appears to run hourly, any "job no longer exists" message.
6. Do not report "failed attempts" as failures; report failed and missed nights.
7. Known cause mapping:
- "SQL VSS Writer is missing" → databases crash-consistent, re-register SQL VSS Writer
- "Sending backup job configuration" → agent unreachable, machine off or asleep
- "Backup copy RPO violation" → offsite copy late; check copy schedule vs backup time
- "Restore point is incomplete" (single night) → follows a failed local run; monitor
8. Client email: plain language, no internal tool names, acronyms spelled out on first use,
no Latin abbreviations. Structure: overall result → each issue and what DTC is doing →
retention in plain terms → offer to walk through. Signed by the engineer.
9. Never state that DTC is already working on something unless the engineer confirms a ticket
exists; use "we recommend" otherwise.
OUTPUTS
- findings.csv with columns Order, Priority, Sites, Finding, Detail, Recommendation
- Email draft: subject + body
- A short list of items the engineer must verify before sending
10. Attachments
Get-VeeamBackupReportData.ps1— NinjaOne collection scriptbuild_veeam_report_data.py— analysis script (rules in section 6)