Skip to main content

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:

  1. How often is each server backed up? (configured schedule, observed pattern, retention, offsite copy)
  2. 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.

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.exe automatically. If missing: winget install Microsoft.PowerShell.
  • Python 3 with pandas and reportlab (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

  1. In NinjaOne, open the client organization and select every BDR device (DTCBSURE-xxxx).
  2. 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, default 21.
    • -OutputPath — default C:\ProgramData\DTC\VeeamReport.
  3. 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 RetainRestorePoints value is ambiguous on VBR 13 (it returned 7 everywhere). 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

  1. 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.
  2. 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.
  3. 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 → filter systemName starting DTCBSURE
  • Run a script: POST /v2/device/{deviceId}/script/run with {"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_id keeps 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=21 or 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 script
  • build_veeam_report_data.py — analysis script (rules in section 6)