Automating File Transfers with WinSCP: A Practical Guide
WinSCP is the canonical Windows tool for scripted SFTP / FTP automation. The basics work in five minutes; the production- reliable version takes longer. This is the practical guide: the WinSCP.com CLI, scripted session syntax, .NET assembly for deeper integration, logging, SSH key auth, and the failure-mode design that keeps scheduled jobs running without 3 AM pages.
WinSCP is the canonical Windows tool for scripted SFTP / FTP automation. The graphical client gets the headlines, but the WinSCP.com command-line companion and the WinSCP .NET assembly are what make it the go-to for scheduled file-transfer jobs on Windows servers. This post walks through both, plus the production concerns — logging, SSH key authentication, error handling, and the failure-mode patterns — that turn a working script into a reliable one.
For PowerShell / Python / Node automation, WinSCP isn't the only path: paramiko (Python), ssh2 (Node), and the OpenSSH-bundled sftp and scp CLIs all handle the same workloads. WinSCP wins on Windows-specific shops because it integrates cleanly with batch files, Task Scheduler, and existing .NET applications.
Two paths: script-mode vs .NET assembly
WinSCP ships two components for automation:
WinSCP.com— the command-line companion to the graphical app. Accepts a sequence of commands (either piped on stdin or read from a script file) and executes them in a single session. Best for shell-script-style automation.WinSCPnet.dll— a .NET assembly that exposes aSessionobject with explicit methods (PutFiles,GetFiles,RemoveFiles, etc.) and event handlers. Best for embedding file transfer in C# / PowerShell / VB.NET applications.
For a one-off scheduled job, the CLI is enough. For deeper integration with an existing app, the .NET assembly is cleaner.
The CLI path: WinSCP.com
A minimal example — connect, upload a file, disconnect:
WinSCP.com /command ^
"open sftp://alice@sftp.example.com/ -privatekey=C:\keys\alice.ppk" ^
"put C:\reports\daily.csv /reports/" ^
"exit"
The ^ is the Windows batch-file line continuation. The same script in a .txt file (one command per line) gets passed via /script=path-to-file.txt.
Real-world structure for a scheduled job:
@echo off
set LOG=C:\logs\daily-upload-%DATE:~10,4%%DATE:~4,2%%DATE:~7,2%.log
WinSCP.com ^
/log="%LOG%" ^
/loglevel=1 ^
/command ^
"open sftp://alice@sftp.example.com/ -privatekey=C:\keys\alice.ppk -hostkey=""ssh-ed25519 256 SHA256:abc123...""" ^
"lcd C:\reports" ^
"cd /reports" ^
"put daily.csv" ^
"close" ^
"exit"
if errorlevel 1 (
echo Transfer failed - see log: %LOG%
powershell -Command "Send-MailMessage -To 'ops@example.com' -Subject 'Daily upload failed' -Body 'See %LOG%' -SmtpServer mail.example.com"
exit /b 1
)
echo Transfer completed successfully
Three production-grade additions in that example:
- Date-stamped log files (
/log=...) so each run gets its own file. - Pinned host key (
-hostkey=...) so the script fails loudly if the server's key changes, rather than silently trusting whatever's on the wire. - Exit code check (
errorlevel) so the script reports failure to its caller and notifies on error.
The .NET assembly path
For deeper integration, the WinSCP .NET assembly exposes a typed Session API. From PowerShell:
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::Sftp
HostName = "sftp.example.com"
UserName = "alice"
SshPrivateKeyPath = "C:\keys\alice.ppk"
SshHostKeyFingerprint = "ssh-ed25519 256 SHA256:abc123..."
}
$session = New-Object WinSCP.Session
try {
$session.Open($sessionOptions)
$transferResult = $session.PutFiles("C:\reports\daily.csv", "/reports/")
$transferResult.Check()
foreach ($transfer in $transferResult.Transfers) {
Write-Host "Uploaded: $($transfer.FileName)"
}
} catch {
Write-Error "Transfer failed: $_"
exit 1
} finally {
$session.Dispose()
}
The .NET assembly gives you per-file event handlers, real exception objects, structured transfer results, and synchronization helpers (one-way mirror, two-way sync). Worth the extra setup if you're building anything beyond a one-shot transfer.
Production concerns: the three things that actually matter
The protocol-level part of WinSCP scripting is the easy part. The production-grade part comes down to three things:
1. Logging
WinSCP's logging is genuinely good — verbose, well-structured, includes the SFTP protocol exchange. Turn it on from day one:
/log="C:\logs\winscp-%DATE:~10,4%%DATE:~4,2%%DATE:~7,2%.log" /loglevel=1
loglevel=1 is "normal" (transfer + connection events). loglevel=2 adds debug info — useful when something's broken. Avoid loglevel=0 (errors only) in production; you lose the context that makes debugging possible.
Log rotation: WinSCP supports /log=...,size,count syntax for size-based rotation. Or use the Windows date stamp in the filename (as above) and a separate cleanup job to delete files older than N days.
2. SSH key authentication
Plain-text passwords in batch files are a security and operational liability — they leak to logs, they get committed to source control by accident, they require coordinated rotation when changed. Use SSH keys instead.
In WinSCP, keys are stored in PuTTY's .ppk format. Either:
- Generate the key in WinSCP itself (Tools → Run PuTTYgen → Generate → Save private key).
- Generate with
ssh-keygen -t ed25519on Linux/macOS and let WinSCP convert on import.
Install the public key on the server (via the server's admin UI or by appending to ~/.ssh/authorized_keys). Then reference the private key file in your script:
-privatekey=C:\keys\alice.ppk
For an interactive walkthrough, see our import-ssh-keys-winscp guide.
For automation that runs unattended, generate the key without a passphrase — otherwise the script will hang waiting for input. For interactive use, a passphrase + Pageant (the PuTTY SSH agent) is the safer pattern.
3. Failure handling
The script will fail. Plan for it.
Common failure modes:
- Source file is missing (upstream system didn't produce it).
- Destination has changed (server reinstalled, host key rotated, credentials revoked).
- Network is intermittent (transient packet loss, ISP issue, server overloaded).
- Account storage limit hit.
- Disk full on the destination.
- Server's passive port range changed.
Handle each one:
- Check exit codes. WinSCP.com returns 0 on success, non-zero on any failure. Batch files:
if errorlevel 1 .... PowerShell:if ($LASTEXITCODE -ne 0) {...}. .NET assembly:try/catcharoundTransferResult.Check(). - Send notifications on failure. Email, Slack webhook, Teams webhook — anything that gets attention before the next scheduled run. Don't rely on someone noticing missing files.
- Make the script idempotent. Re-running the same script after a failure should be safe — same files transferred, no duplicate work. Resume support (WinSCP has it) makes this cleaner.
- Pin the host key.
-hostkey="ssh-ed25519 256 SHA256:..."in the open command means the script fails loudly if the server's host key changes. Without this, a server-reinstall scenario silently trusts whatever's at the IP. - Limit retry. Infinite-retry loops on a hard failure consume resources and delay the eventual human investigation. Three tries with exponential backoff, then alert.
Scheduling: Windows Task Scheduler vs alternatives
Windows Task Scheduler is the default home for WinSCP automation:
- Action: Start a program —
WinSCP.comwith/script=path-to-script.txt. - Trigger: time-based or event-based.
- Run as: a service account, not your interactive user.
- Settings: "Run whether user is logged on or not" + "Run with highest privileges" if the script needs file-system access to system directories.
For more sophisticated scheduling — dependencies between jobs, retry logic, observability — consider a real orchestrator: Windows-side (Azure Logic Apps, Power Automate Desktop), cross-platform (Apache Airflow, Prefect, Dagster), or CI/CD systems (Azure DevOps Pipelines, GitHub Actions self-hosted runners).
When WinSCP scripting is the wrong tool
Three situations where you'll want a different tool:
- Non-Windows servers. WinSCP doesn't run on Linux or macOS. Use
lftp,rsync over SSH, OpenSSHsftp, or a language library instead. - Event-driven workflows. "When a file arrives, do X." WinSCP scripts run on a schedule, not in response to events. Use webhooks from your file-transfer server instead — Files.com and most managed platforms support this.
- Complex routing and transformation. "Pull from server A, run a transformation, push to server B and C, send a Slack message." Worth using a real automation platform rather than a chain of WinSCP scripts.
The modern way: skip the scripting
Files.com is the File Orchestration Platform we'd recommend for shops whose scripted-WinSCP automation has gotten complex. The platform provides:
- Built-in automation workflows — "when a file arrives in folder X, do Y" without writing scripts.
- Webhooks on every file event — kick off downstream automation in real time.
- REST API + SDKs in 8 languages for embedding file transfer in apps without subprocess-ing WinSCP.com.
- Audit logging across every operation — replaces ad-hoc log files with queryable history.
- MFA, SSO, IP allowlisting — enterprise security without per-script wiring.
Start a free Files.com trial — no credit card.
For Windows-centric environments that must run file-transfer infrastructure inside their own datacenter, the free ExaVault on-premise appliance handles SFTP / FTPS / FTP / WebDAV with the same automation surface from a self-hosted VM image.
FAQ
Can WinSCP run scheduled file transfers?
Yes. Pair WinSCP.com with Windows Task Scheduler — the Action runs WinSCP.com /script=your-script.txt on whatever schedule you configure. For event-driven transfers (run when a file arrives), use the file-transfer server's webhooks instead of polling from WinSCP.
What's the difference between WinSCP.com and WinSCP.exe?
WinSCP.exe is the graphical client. WinSCP.com is the command-line companion for scripted use — same protocol stack, but takes commands from stdin or a script file instead of a UI. Both ship in the same install.
How do I use SSH keys in a WinSCP script?
Generate or import an SSH key into PuTTY's .ppk format (WinSCP converts OpenSSH-format keys automatically). Reference it in the open command:
open sftp://alice@sftp.example.com/ -privatekey=C:\keys\alice.ppk
For unattended automation, generate the key without a passphrase. See our SSH-keys-in-WinSCP walkthrough.
How do I check if a WinSCP script succeeded?
Check the exit code. In a batch file: if errorlevel 1 (echo failed). In PowerShell: if ($LASTEXITCODE -ne 0) {...}. The .NET assembly throws exceptions on failure, which you catch with try / catch.
Can WinSCP scripts use SFTP and FTP?
Yes — change sftp:// to ftp:// (or ftps:// for FTP over TLS) in the open command, and adjust the port accordingly. Same script structure for all three protocols.
Where do I find WinSCP script logs?
Set the log path in the script with /log="C:\path\to\log.txt". Log level: /loglevel=1 for normal verbosity (recommended), /loglevel=2 for debug. Without /log, WinSCP doesn't write logs at all.
Can I automate WinSCP from PowerShell?
Yes — use the WinSCP .NET assembly instead of subprocess-ing WinSCP.com. Load the DLL with Add-Type -Path "...\WinSCPnet.dll" and use the typed Session, SessionOptions, and TransferOptions classes. Cleaner error handling, better integration with the rest of your PowerShell scripts.
Does WinSCP scripting support resume?
Yes. The put and get commands accept a -resume flag (or you can set it as a default in option transfer resume). On a failed transfer, the next run with resume enabled picks up from the partial file. See our FTP resume guide.