FTP and SFTP automation in Power Automate Desktop goes far beyond drag-and-drop upload actions — production-grade workflows require protocol awareness, secure credential management, per-file verification, and layered error handling. This lesson teaches you to build unattended transfer automation that handles real-world failures reliably, including the PowerShell/WinSCP path for SFTP that PAD's native actions don't cover.

Picture this: your organization generates nightly export files from three different legacy systems — purchase orders from an ERP, inventory snapshots from a warehouse management system, and reconciliation reports from a finance platform. Each of these needs to land on a trading partner's SFTP server by 6:00 AM, every business day, without anyone manually opening FileZilla and dragging files into a remote directory. When something goes wrong — a file is missing, the server is unreachable, or a transfer silently fails — you need the automation to detect it, retry intelligently, log the failure, and notify someone before the trading partner's batch job starts at 7:00 AM.
This is exactly the kind of mission-critical, unattended file transfer workflow that Power Automate Desktop can own entirely. But doing it well requires more than just dropping an "FTP Upload" action into a flow. You need to understand the protocol differences between FTP and SFTP, manage credentials securely, handle the dozen ways a transfer can fail silently, build robust retry logic, and structure everything so it runs reliably as an unattended RPA job on a headless machine at 5:30 AM.
By the end of this lesson, you'll have the knowledge and a working blueprint to build production-grade FTP/SFTP automation that handles real-world complexity. This isn't a happy-path tutorial — we'll cover the edge cases, the security implications, and the architectural decisions that separate a bot that works in testing from one that runs flawlessly in production for months.
What you'll learn:
Before diving in, you should be comfortable with the following:
You'll also need access to an FTP or SFTP server to test against. For development, free tools like FileZilla Server (for FTP/FTPS) or OpenSSH on Windows (for SFTP) work well locally.
Let's clear up something that trips up a lot of automation engineers: FTP, FTPS, and SFTP are three fundamentally different protocols, and treating them as interchangeable will cause you real pain.
FTP (File Transfer Protocol) is the original, dating to 1971. It uses two separate TCP connections — a control channel on port 21 and a data channel on a dynamically negotiated port. It transmits credentials and data in plaintext. In 2024, no legitimate business partner should be asking you to transfer files over plain FTP, but legacy systems still exist, so we'll cover it.
FTPS (FTP Secure) adds TLS encryption to FTP. It still uses the same dual-channel architecture, which creates firewall headaches (particularly with passive mode port ranges), but your credentials and data are encrypted. It runs on port 21 (explicit FTPS, where TLS is negotiated after connection) or port 990 (implicit FTPS, where TLS is mandatory from the start).
SFTP (SSH File Transfer Protocol) is an entirely different beast — it has nothing to do with FTP despite the name similarity. SFTP runs over SSH on port 22, uses a single encrypted channel for everything, and supports key-based authentication. Most modern trading partners and cloud storage systems use SFTP. It's what you should prefer whenever you have a choice.
Why does this matter for Power Automate Desktop? Because PAD's native FTP actions support FTP and FTPS only. SFTP support is notably absent from the built-in action library. This is a significant gap that forces you to choose between PowerShell scripting (using WinSCP or Posh-SSH modules) or third-party PAD connectors for true SFTP connectivity. We'll cover both paths.
Key insight
Don't assume "FTP actions" in PAD will handle your SFTP connection. Check your trading partner's requirements carefully. If they give you a host, port 22, and an SSH key or password, that's SFTP and you'll need the PowerShell path described later in this lesson.
Before you start dragging actions into the canvas, design your flow architecture on paper. A well-structured FTP/SFTP automation for unattended use has several distinct layers:
1. Initialization — Load configuration (server details, paths, file patterns), validate prerequisites (local files exist, credentials are available), set up logging variables.
2. Pre-transfer validation — Confirm the source directory has the expected files, check file ages to catch stale data, optionally verify file sizes are non-zero.
3. Connection — Establish the FTP/SFTP session. This step deserves its own error boundary since connection failures need different handling than mid-transfer failures.
4. Transfer loop — Iterate through files, upload each one, verify the transfer (check remote file size matches local), and track results per file.
5. Post-transfer actions — Archive uploaded files locally, clean up remote directories if needed, write final status to a log file or database.
6. Notification and alerting — Send success/failure summaries via email, update a SharePoint list, or trigger a cloud flow with results.
This layered approach maps naturally onto Subflows and Reusable Logic in Power Automate Desktop. Break each layer into a named subflow: Initialize, ValidateFiles, ConnectFTP, TransferFiles, PostTransfer, and SendAlert. Your main flow becomes a clean orchestration sequence that calls each subflow in order with shared variables flowing between them.
The benefit of this architecture isn't just cleanliness — it's that you can implement targeted error handling at each layer. A connection failure triggers a different recovery path than a transfer failure for one file out of fifteen.
Power Automate Desktop includes a built-in FTP action group. Let's walk through what's available and how to use it properly.
The Open FTP connection action requires:
The action produces an FTPConnection variable that you pass to every subsequent FTP action. This is analogous to a database connection object — keep it, reuse it, and close it explicitly when done.
# Variable assignments in PAD notation:
FTPHost = "ftp.tradingpartner.com"
FTPPort = 21
FTPUsername = %CredentialUsername% # Loaded from secure variable
FTPPassword = %CredentialPassword% # Loaded from secure variable or Key Vault
Warning
Never hardcode FTP credentials as literal string values in your flow. At minimum, use PAD's Input variables marked as sensitive. For production unattended flows, retrieve credentials from Azure Key Vault at runtime — the pattern is explained in detail in Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault. A hardcoded password in a desktop flow is visible to anyone with edit access to the flow.
When building the Open FTP connection action, set the Timeout property. The default is often 30 seconds, but corporate network conditions — VPNs, proxy servers, network security appliances that inspect traffic — can introduce latency. For trading partner connections over the internet, 60 seconds is more realistic. Setting this too low causes spurious timeout errors that look like server availability problems.
Before uploading, it's good practice to list the remote directory and confirm your target path exists. Use List FTP directory to retrieve the contents:
FTP List directory action:
FTP Connection: FTPConnection
Directory: /incoming/purchase_orders/
→ Store result in: RemoteFileList
The result is a list of FTP file objects with properties including Name, Size, LastModified, and IsFolder. You can inspect this list to verify the directory exists (if the action throws an error, the path likely doesn't exist) or to check for naming conflicts before uploading.
The Upload file(s) to FTP action handles single files or can accept wildcard patterns. For batch uploads, you have two approaches:
Option A — Upload all matching files at once using a wildcard:
Upload file(s) to FTP action:
FTP Connection: FTPConnection
File(s) to upload: C:\Exports\PO_*.csv
Remote path: /incoming/purchase_orders/
If file exists: Overwrite
Option B — Loop and upload individually (preferred for production):
Get files in folder action:
Folder: C:\Exports\PurchaseOrders\
File filter: PO_*.csv
→ Store in: LocalFileList
FOR EACH CurrentFile IN LocalFileList:
Upload file(s) to FTP action:
FTP Connection: FTPConnection
File(s) to upload: %CurrentFile.FullName%
Remote path: /incoming/purchase_orders/
If file exists: Overwrite
# Immediately verify the upload
List FTP directory action:
FTP Connection: FTPConnection
Directory: /incoming/purchase_orders/
→ Store in: PostUploadList
# Find the file we just uploaded in the remote listing
...verify file size matches...
# Archive the local file
Move file action:
File to move: %CurrentFile.FullName%
Destination: C:\Exports\Archive\
Option B is significantly more robust because it lets you handle each file's success or failure independently. If one file fails to upload, the loop continues with the remaining files, and your failure tracking records exactly which file caused the problem. Option A treats the entire batch as an atomic unit — one failure potentially stops everything.
One of the most dangerous assumptions in FTP automation is that a successful upload action means the file arrived correctly. FTP has no built-in integrity verification — the action completes when the data is sent, not when the server confirms it wrote correctly.
After each upload, retrieve the remote file listing for that directory and compare the uploaded file's remote size to the local file's size:
Get file info action:
File: %CurrentFile.FullName%
→ Store in: LocalFileInfo
# Find matching file in RemoteFileList
FOR EACH RemoteFile IN PostUploadList:
IF RemoteFile.Name = CurrentFile.Name THEN:
IF RemoteFile.Size <> LocalFileInfo.Size THEN:
SET TransferVerified = false
SET FailureReason = "Size mismatch: Local=" + LocalFileInfo.Size + " Remote=" + RemoteFile.Size
ELSE:
SET TransferVerified = true
END IF
END IF
END FOR
Tip
For text files (CSV, XML, JSON), a size mismatch might legitimately occur due to line-ending conversion during transfer (FTP's ASCII mode converts CRLF to LF). Either transfer in BINARY mode always, or account for the expected size difference in your verification logic. Most modern FTP servers handle this gracefully in binary mode.
Always close the FTP connection explicitly with the Close FTP connection action. Place this in a cleanup section that runs regardless of whether the transfer succeeded or failed — the same pattern as closing a database connection. Leaving connections open accumulates against server-side limits and can prevent future automation runs from connecting.
Since PAD lacks native SFTP actions, PowerShell is your primary tool. The two main options are:
WinSCP .NET Assembly — WinSCP is a mature, widely-used SFTP client with excellent .NET integration. It supports SFTP, FTP, FTPS, SCP, and WebDAV, handles SSH key authentication, and provides comprehensive transfer verification.
Posh-SSH PowerShell Module — A pure PowerShell module that wraps SSH/SFTP functionality, installable from the PowerShell Gallery.
WinSCP is the better choice for production RPA for several reasons: it has superior error reporting, built-in transfer verification (checksum-based), a robust session object model, and excellent logging. Posh-SSH is simpler but more limited.
On the unattended RPA machine, install WinSCP (the standard installer, not just the portable version) so the .NET assembly is available. Then confirm the assembly path:
C:\Program Files (x86)\WinSCP\WinSCPnet.dll
If you're deploying across multiple machines in a machine group, use a startup script or configuration management (SCCM, Intune) to ensure WinSCP is installed consistently. For more on machine group management, see Managing Machines and Machine Groups for Scalable Unattended Automation in Power Automate.
In PAD, use the Run PowerShell script action to execute SFTP operations. Here's a complete, production-ready script for batch SFTP uploads with verification:
# Parameters passed from PAD variables
param(
[string]$SFTPHost,
[string]$SFTPUsername,
[string]$SFTPPassword,
[string]$SFTPPort = "22",
[string]$RemotePath,
[string]$LocalFolder,
[string]$FilePattern,
[string]$SSHHostKeyFingerprint,
[string]$ArchiveFolder
)
# Output tracking
$Results = @()
$OverallSuccess = $true
try {
# Load WinSCP .NET assembly
$WinSCPPath = "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
Add-Type -Path $WinSCPPath
# Configure session options
$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::Sftp
HostName = $SFTPHost
UserName = $SFTPUsername
Password = $SFTPPassword
PortNumber = [int]$SFTPPort
SshHostKeyFingerprint = $SSHHostKeyFingerprint
}
$Session = New-Object WinSCP.Session
# Enable transfer logging to a temp file
$LogPath = "C:\RPA\Logs\WinSCP_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$Session.SessionLogPath = $LogPath
# Open the SFTP session
$Session.Open($SessionOptions)
# Get files matching the pattern
$FilesToUpload = Get-ChildItem -Path $LocalFolder -Filter $FilePattern
if ($FilesToUpload.Count -eq 0) {
Write-Output "NO_FILES_FOUND"
exit 0
}
foreach ($File in $FilesToUpload) {
$FileResult = [PSCustomObject]@{
FileName = $File.Name
LocalSize = $File.Length
Status = "Pending"
ErrorDetail = ""
}
try {
# Build transfer options with verification
$TransferOptions = New-Object WinSCP.TransferOptions
$TransferOptions.TransferMode = [WinSCP.TransferMode]::Binary
$TransferOptions.ResumeSupport.State = [WinSCP.TransferResumeSupportState]::Off
# Upload the file
$TransferResult = $Session.PutFiles(
$File.FullName,
($RemotePath.TrimEnd("/") + "/" + $File.Name),
$false, # Don't remove source file
$TransferOptions
)
# Check for transfer errors
$TransferResult.Check()
# Verify remote file exists and size matches
$RemoteFileInfo = $Session.GetFileInfo($RemotePath.TrimEnd("/") + "/" + $File.Name)
if ($RemoteFileInfo.Length -ne $File.Length) {
throw "Size mismatch: local=$($File.Length), remote=$($RemoteFileInfo.Length)"
}
# Archive the local file
if (![string]::IsNullOrEmpty($ArchiveFolder)) {
$ArchivePath = Join-Path $ArchiveFolder $File.Name
Move-Item -Path $File.FullName -Destination $ArchivePath -Force
}
$FileResult.Status = "Success"
}
catch {
$FileResult.Status = "Failed"
$FileResult.ErrorDetail = $_.Exception.Message
$OverallSuccess = $false
}
$Results += $FileResult
}
$Session.Close()
}
catch {
# Session-level failure (connection, authentication, etc.)
Write-Output "SESSION_ERROR: $($_.Exception.Message)"
exit 1
}
# Output structured results as JSON for PAD to parse
$Output = [PSCustomObject]@{
OverallSuccess = $OverallSuccess
Results = $Results
TotalFiles = $Results.Count
SuccessCount = ($Results | Where-Object { $_.Status -eq "Success" }).Count
FailureCount = ($Results | Where-Object { $_.Status -eq "Failed" }).Count
}
Write-Output ($Output | ConvertTo-Json -Depth 5)
In PAD, call this with the Run PowerShell script action and capture the ScriptOutput variable. Then parse the JSON output:
Run PowerShell script action:
Script: %PowerShellScriptContent%
Script parameters:
-SFTPHost %SFTPHost%
-SFTPUsername %SFTPUsername%
-SFTPPassword %SFTPPassword%
-RemotePath "/incoming/purchase_orders/"
-LocalFolder "C:\Exports\PurchaseOrders\"
-FilePattern "PO_*.csv"
-SSHHostKeyFingerprint %HostKeyFingerprint%
-ArchiveFolder "C:\Exports\Archive\"
→ PowerShell output: ScriptOutput
→ PowerShell exit code: ScriptExitCode
Warning
Notice the SshHostKeyFingerprint parameter. Never use GiveUpSecurityAndAcceptAnySshHostKey = $true in production — this disables host verification and leaves you vulnerable to man-in-the-middle attacks. Obtain the legitimate fingerprint from your server administrator and store it as a flow variable. The fingerprint format WinSCP expects looks like: ssh-rsa 2048 xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx.
Many SFTP servers require key-based authentication rather than passwords. WinSCP handles this with the SshPrivateKeyPath property on SessionOptions. The private key needs to be in PuTTY format (.ppk). If your trading partner gives you an OpenSSH format key, convert it using WinSCP's built-in key conversion tool.
For unattended flows, the private key file needs to be present on the automation machine. Store it in a protected directory (not world-readable) and reference its path in your script. Pass the path as a parameter from PAD rather than hardcoding it, so you can change the location without modifying the script.
# Add to SessionOptions when using key auth
$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::Sftp
HostName = $SFTPHost
UserName = $SFTPUsername
SshPrivateKeyPath = $SSHKeyPath
SshPrivateKeyPassphrase = $KeyPassphrase # if the key has a passphrase
PortNumber = [int]$SFTPPort
SshHostKeyFingerprint = $SSHHostKeyFingerprint
}
Detailed guidance on securing sensitive values like key passphrases in PAD flows is covered in Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault — the same patterns apply here.
File transfer failures come in several distinct categories, and your error handling needs to treat them differently:
Category 1: Connection failures — The server is unreachable, DNS resolution fails, the port is blocked, or the SSL/TLS handshake fails. These are typically transient (network hiccup) or permanent (wrong host). Retry 2-3 times with exponential backoff before declaring failure.
Category 2: Authentication failures — Wrong credentials, expired password, IP whitelist violation, SSH key mismatch. Retrying immediately is futile and risks account lockout. Alert immediately, don't retry.
Category 3: Partial transfer failures — The session connected, some files uploaded successfully, but one or more failed. This is the most complex case — you must not re-upload the successful files, but must retry or report the failures.
Category 4: Post-transfer verification failures — The upload action "succeeded" but size verification shows a mismatch. Treat these as partial failures; delete the corrupt remote file if possible and retry.
Category 5: No source files found — The local directory has no files matching the pattern. This might be normal (nothing to send today) or an upstream process failure. Your handling depends on business rules.
Use nested On block error handlers to implement this categorization. The outer block catches connection-level errors; the inner block (inside the transfer loop) catches per-file errors:
ON BLOCK ERROR:
# Handle connection failure
IF ErrorMessage CONTAINS "Authentication" OR ErrorMessage CONTAINS "password":
SET ErrorCategory = "AUTH_FAILURE"
SET ShouldRetry = false
ELSE IF ErrorMessage CONTAINS "timeout" OR ErrorMessage CONTAINS "connection refused":
SET ErrorCategory = "CONNECTION_FAILURE"
SET ShouldRetry = true
SET RetryDelaySeconds = 30
ELSE:
SET ErrorCategory = "UNKNOWN_FAILURE"
SET ShouldRetry = true
SET RetryDelaySeconds = 60
END IF
Call subflow: LogError (ErrorCategory, ErrorMessage, FlowContext)
IF ShouldRetry AND RetryAttempt < MaxRetries:
SET RetryAttempt = RetryAttempt + 1
Wait action: RetryDelaySeconds seconds
GOTO ConnectionBlock
ELSE:
Call subflow: SendFailureAlert
EXIT FLOW
END IF
END ON BLOCK ERROR
For per-file errors inside the transfer loop, don't let a single file failure kill the entire run. Instead, track failures in a data table and continue:
DATA TABLE: TransferResults
Columns: FileName, Status, ErrorMessage, Timestamp
FOR EACH FileToUpload IN LocalFileList:
ON BLOCK ERROR:
Add row to TransferResults: [FileToUpload.Name, "FAILED", LastErrorMessage, Now()]
SET FailureCount = FailureCount + 1
CONTINUE # Move to next file
END ON BLOCK ERROR
# Upload logic here...
Add row to TransferResults: [FileToUpload.Name, "SUCCESS", "", Now()]
SET SuccessCount = SuccessCount + 1
END FOR
After the loop, evaluate overall success and decide whether to alert:
IF FailureCount > 0:
IF SuccessCount = 0:
# Total failure - all files failed
Call subflow: SendCriticalAlert (TransferResults)
ELSE:
# Partial success - some files uploaded
Call subflow: SendPartialFailureAlert (TransferResults)
END IF
ELSE:
Call subflow: SendSuccessNotification (SuccessCount, TotalFiles)
END IF
Key insight
The decision of whether a partial failure is "acceptable" is a business rule, not a technical one. In some organizations, 8 out of 9 files uploading successfully is fine — the missing file will be caught in reconciliation. In others, every file must arrive or the entire batch should be considered failed. Parameterize this threshold (e.g., MinSuccessRatePct = 100) so the business can change it without modifying the flow.
Simple "wait 30 seconds and retry" logic works, but exponential backoff is more sophisticated and polite to the server:
# In the retry block:
SET WaitSeconds = BaseRetrySeconds * (2 ^ (RetryAttempt - 1))
# Retry 1: 30 seconds, Retry 2: 60 seconds, Retry 3: 120 seconds
# Add jitter to avoid thundering herd if multiple bots retry simultaneously
SET JitterSeconds = [random number between 0 and 15]
SET ActualWaitSeconds = WaitSeconds + JitterSeconds
Wait action: ActualWaitSeconds seconds
Power Automate Desktop's Generate random number action can provide the jitter. This matters when you have multiple bots in a machine group all hitting the same FTP server after a brief outage.
An unattended flow that doesn't produce auditable logs is one you can't troubleshoot when it fails at 5:30 AM and you're asleep. Build structured logging from the start.
Write a delimited log file that records every significant event:
Timestamp|Level|FlowName|EventType|FileName|Status|Detail
2024-03-15 05:31:02|INFO|SFTP_NightlyTransfer|FLOW_START|||Starting transfer run for 2024-03-15
2024-03-15 05:31:04|INFO|SFTP_NightlyTransfer|FILES_FOUND||9|Found 9 files in C:\Exports\PurchaseOrders\
2024-03-15 05:31:05|INFO|SFTP_NightlyTransfer|CONNECTION||SUCCESS|Connected to ftp.tradingpartner.com:22
2024-03-15 05:31:07|INFO|SFTP_NightlyTransfer|UPLOAD|PO_20240315_001.csv|SUCCESS|Size: 48291 bytes
2024-03-15 05:31:09|ERROR|SFTP_NightlyTransfer|UPLOAD|PO_20240315_004.csv|FAILED|Timeout after 60s
2024-03-15 05:31:45|INFO|SFTP_NightlyTransfer|UPLOAD|PO_20240315_004.csv|SUCCESS|Retry 1 succeeded
2024-03-15 05:32:18|INFO|SFTP_NightlyTransfer|FLOW_END|||Completed: 9/9 files, 1 retry
Use PAD's Write text to file action in append mode to add each log entry. Build a LogEntry subflow that formats the timestamp and writes the line — call it from every point in your flow that needs logging.
For enterprise deployments, writing logs to a local file isn't sufficient. You want a queryable audit trail. The pattern for this depends on your architecture:
Cloud flow trigger: If your desktop flow is triggered from a cloud flow, return the TransferResults data table as an output variable and let the cloud flow write it to SharePoint, Dataverse, or Azure SQL.
Direct HTTP: Use PAD's Invoke web service action to POST log entries to a Power Automate HTTP trigger or an Azure Function endpoint.
Database: Use an ODBC connection and PAD's SQL actions to INSERT audit records directly. This approach is fast and creates a proper audit trail for compliance purposes. See Automating Database Queries and Record Updates from Power Automate Desktop for the implementation pattern.
Before opening a single FTP connection, validate that your source files are ready. This is especially important in chained automation where an upstream process generates the files:
# Check 1: Do files exist?
Get files in folder:
Folder: C:\Exports\PurchaseOrders\
File filter: PO_*.csv
→ LocalFileList
IF LocalFileList.Count = 0:
IF (today is a business day):
# This is probably an upstream failure
Call subflow: AlertMissingFiles
EXIT FLOW with error
ELSE:
# Weekends/holidays - no files expected
Call subflow: LogNoFilesExpected
EXIT FLOW normally
END IF
END IF
# Check 2: Are files non-zero size?
FOR EACH File IN LocalFileList:
IF File.Size = 0:
Call subflow: AlertZeroByteFile (File.Name)
Remove File from LocalFileList
END IF
END FOR
# Check 3: Are files recent enough? (guard against stale files being re-sent)
FOR EACH File IN LocalFileList:
IF File.LastModified < (Now() - 25 hours):
Call subflow: AlertStaleFile (File.Name, File.LastModified)
# Depending on policy: either exclude or alert-and-continue
END IF
END FOR
The business-day check requires knowing your organization's holiday calendar. A pragmatic approach: maintain a CSV file of business days (or non-business days) that you check at runtime. The Reading and Writing to CSV and Text Files in Power Automate Desktop article covers the mechanics of reading that file efficiently.
After confirmed successful upload, move local files to an archive directory with a date-stamped subfolder:
# Create dated archive subdirectory
SET ArchiveDir = "C:\Exports\Archive\" + Format(Today(), "yyyy-MM-dd")
Create folder if not exists: ArchiveDir
FOR EACH SuccessfulFile IN SuccessfulTransfers:
Move file:
Source: %SuccessfulFile.FullName%
Destination: ArchiveDir + "\" + SuccessfulFile.Name
If exists: Rename (append timestamp)
END FOR
Tip
Don't delete source files — archive them. A hard delete means you have no recovery path if the receiving system reports the file was corrupted or never processed. Keep archives for at least 30 days (or whatever your data retention policy specifies) and implement a separate cleanup automation that purges archives older than the retention period.
If your automation runs twice on the same day (due to a retry or manual re-run), you'll hit naming conflicts in the archive. Append a timestamp suffix to disambiguate:
SET ArchiveFileName = File.NameWithoutExtension + "_" + Format(Now(), "HHmmss") + File.Extension
The entire flow only works as designed if it runs reliably unattended. Several configuration concerns deserve specific attention:
For nightly runs, you have two options:
Option 2 is generally preferred because it keeps everything within the Power Automate ecosystem and gives you better monitoring through the Power Automate portal. The scheduling and triggering pattern is covered in depth in Triggering Desktop Flows from Cloud Flows: Passing Inputs and Returning Outputs.
The automation machine needs specific configuration for unattended FTP/SFTP:
SessionOptions; PAD's native FTP actions respect system proxy settingsRemoteSigned or Bypass for the machine account that runs the unattended bot — the default Restricted policy will prevent your PowerShell scripts from runningWarning
Unattended flows run under the service account configured for the machine, not your personal Windows account. Always test your PowerShell scripts explicitly under that service account (use Run as different user or runas) before deploying. File permissions, module availability, and environment variables all differ between accounts.
If you're running multiple FTP flows on different machines or handling multiple trading partners, be deliberate about concurrency. Most FTP servers enforce maximum concurrent session limits per user. If three bots try to authenticate simultaneously with the same credentials, two will fail.
Handle this with Power Automate's queue-based dispatch or by staggering trigger times. For more on orchestrating flows across machine groups, see Building a Resilient Unattended RPA Orchestration Framework in Power Automate Desktop.
Build a complete FTP/SFTP transfer flow for the following scenario:
Scenario: Your logistics team generates daily shipment confirmation files (CSV format, named SHIP_YYYYMMDD_NNN.csv) in C:\EDI\Outbound\ShipmentConfirm\. These must be uploaded to a trading partner's SFTP server at sftp.logistics-partner.com (port 22) in the /edi/inbound/ship_confirm/ directory by 7:00 AM each weekday.
Build the following components:
Initialize subflow: Load SFTP credentials from PAD sensitive input variables. Set constants: MaxRetries = 3, BaseRetrySeconds = 30, MinRequiredFiles = 1. Initialize a TransferResults data table with columns: FileName, FileSizeBytes, Status, RetryCount, ErrorDetail, Timestamp.
ValidateFiles subflow: Scan the source directory for SHIP_*.csv files modified in the last 26 hours (to catch files from the previous evening's run). Alert and exit if zero files are found on a weekday. Remove zero-byte files from the transfer list with a warning log entry.
TransferFiles subflow: Using the PowerShell/WinSCP approach, upload each file individually. After each upload, verify the remote file size matches the local size. On failure, retry up to 2 times with 30-second waits. Record each file's outcome in TransferResults.
PostTransfer subflow: Move successfully uploaded files to C:\EDI\Archive\ShipmentConfirm\YYYY-MM-DD\. Write a summary log entry: total files, success count, failure count, total bytes transferred, elapsed time.
SendAlert subflow: If any files failed after all retries, send an email (using PAD's Send email action or Outlook automation) with the TransferResults data table formatted as an HTML table in the email body. If all succeeded, write a success entry to the log file only (no email needed unless configured).
Advanced challenge: Add a pre-upload step that reads the first line of each CSV to validate the header matches the expected schema (ShipmentID,OrderNumber,TrackingNumber,ShipDate,Carrier,Weight). Reject files with incorrect headers before attempting upload.
Symptom: Connection succeeds (you see the 220 banner from the server) but listing directories or uploading files hangs or times out.
Cause: Active FTP mode requires the server to make an inbound connection to your machine on an ephemeral port. Your firewall blocks it.
Fix: Always use passive mode. In PAD's FTP action, look for the "Connection mode" property and set it to Passive. In WinSCP, set SessionOptions.AddRawSettings("FtpMode", "1").
Symptom: SFTP connections start failing with "Host key doesn't match" errors after working for months.
Cause: The trading partner replaced their server certificate, upgraded SSH, or migrated to new hardware. The SSH host key fingerprint changed.
Fix: Build an alert specifically for this error code. When detected, notify an administrator to verify the new fingerprint with the trading partner (call them — don't just accept the new key automatically) and update the stored fingerprint variable. Never auto-accept changed host keys in production.
Symptom: Authentication fails after 90 days with the same credentials that worked before.
Cause: Many SFTP servers enforce password rotation policies. The password expired and wasn't rotated in your secure store.
Fix: Implement a credential validation test that runs weekly (separate from the nightly transfer flow). If it fails, alert the team immediately so they can rotate credentials before the nightly run fails. Better yet, switch to SSH key authentication, which doesn't expire.
Symptom: Small files transfer fine; larger files (>50MB) fail with timeout errors partway through.
Cause: PAD's default FTP action timeout applies to the operation overall, not per-packet. Long transfers on slow connections can exceed it.
Fix: In WinSCP PowerShell scripts, set Session.Timeout = New-TimeSpan -Minutes 30 for large file transfers. For PAD native FTP actions, increase the timeout in the action properties. Also check whether the server has its own idle timeout that disconnects stale sessions.
Symptom: Occasionally, the transfer flow runs before the upstream process finishes writing files. Some files transfer with 0 bytes or partial content.
Cause: The upstream process is still writing files when your transfer flow starts.
Fix: Implement a file lock check or wait-for-stability pattern. Before including a file in the transfer list, record its size, wait 10 seconds, check the size again. If it changed, the file is still being written — wait and check again. Only transfer files that haven't changed size for two consecutive checks. Alternatively, coordinate with the upstream process to write a "ready" sentinel file (e.g., TRANSFER_READY.flag) that you wait for before starting.
Symptom: PowerShell script action returns exit code 1 with an error about scripts being disabled.
Cause: The machine account running the unattended bot has the default Restricted execution policy.
Fix:
# Option 1: Set machine-wide policy (requires admin)
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
# Option 2: Bypass for PAD's inline script only
# In PAD's Run PowerShell script action, prepend:
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
Option 2 is safer for shared machines as it only affects the current process.
Building production-grade FTP/SFTP automation in Power Automate Desktop is an exercise in layered engineering. The transfer itself is the easy part — the hard part is everything around it: distinguishing protocol differences, securing credentials, handling every failure mode distinctly, verifying transfers actually completed, and building logging that lets you diagnose problems at 5:30 AM without logging into the machine.
The key architectural principles to carry forward:
Where to go next:
If your file transfer workflow is part of a larger automation — where transferred files feed downstream processing — explore how to chain flows together and orchestrate multi-step pipelines in Building a Resilient Unattended RPA Orchestration Framework in Power Automate Desktop.
If the files you're transferring come from or feed into Excel reports, the techniques in Automating Excel with Power Automate Desktop: Reading, Writing, and Running Macros will help you build end-to-end data pipelines entirely within Power Automate Desktop.
And if you're looking to scale this across dozens of trading partners and machine groups, the monitoring and visibility infrastructure described in Monitoring and Troubleshooting Desktop Flow Runs at Scale will give you the operational visibility you need to run these workflows confidently in production.
Power Automate Desktop & RPA
Implementing Dynamic Selector Repair and Fallback Strategies in Power Automate Desktop: Detecting Broken UI Elements at Runtime and Switching to Alternative Identification Methods Without Flow Failure
Automating Windows Credential Manager and Vault Operations in Power Automate Desktop: Storing, Retrieving, and Rotating Application Passwords for Secure Unattended Bot Authentication