Master production-grade file and email automation in Power Automate Desktop. Learn how to build reliable pipelines that process incoming files, manage dynamic folder structures, read and send Outlook emails with attachments, and wire everything together into a robust, self-monitoring bot.

Picture this: your finance team receives 40 vendor invoices every morning, dropped into a shared network folder as PDFs, Excel files, and the occasional Word document. Someone — maybe you, maybe someone you manage — opens each one, renames it according to a naming convention, sorts it into the right subfolder by vendor and month, and then fires off a confirmation email. It takes 90 minutes. Every day. That's nearly 400 hours a year of pure mechanical repetition.
File, folder, and email operations are the connective tissue of almost every real-world automation. Even the most sophisticated desktop flow — one that logs into a legacy ERP, scrapes a web portal, or processes database exports — usually starts by picking up a file from somewhere and ends by moving it somewhere else and notifying someone. Yet this "connective tissue" is where most Power Automate Desktop beginners underestimate complexity. Paths that work on their machine break on the production server. Files that should be there aren't. Outlook throws a COM exception because someone left a calendar reminder open. Getting these operations production-grade requires understanding not just the happy path, but the failure modes, edge cases, and design decisions that separate a fragile script from a reliable bot.
By the end of this lesson you will have a deep, working understanding of how Power Automate Desktop handles files, folders, and Outlook email. You'll know how to build flows that are genuinely robust — flows you can hand off to an unattended runner at 2 AM without losing sleep.
What you'll learn:
This article assumes you're working at an expert level with Power Automate Desktop. You should be comfortable with:
You'll also benefit from familiarity with subflows, since we'll be decomposing our pipeline that way (see Subflows and Reusable Logic in Power Automate Desktop).
Before you write a single action, you need to understand what PAD actually hands you when it "gets" a file or folder. This is the gap that causes most beginner failures.
When you use Get files in folder, PAD returns a List of File objects — not strings, not paths, but typed objects. Each File object exposes properties you access via dot notation:
%CurrentFile.Name% → "Invoice_Contoso_2024-11.pdf"
%CurrentFile.NameWithoutExtension% → "Invoice_Contoso_2024-11"
%CurrentFile.Extension% → ".pdf"
%CurrentFile.FullName% → "C:\Invoices\Incoming\Invoice_Contoso_2024-11.pdf"
%CurrentFile.Directory% → "C:\Invoices\Incoming"
%CurrentFile.Size% → 204800 (bytes, as integer)
%CurrentFile.CreationTime% → DateTime object
%CurrentFile.LastModified% → DateTime object
Similarly, Get subfolders in folder returns a List of Folder objects with properties like %CurrentFolder.Name%, %CurrentFolder.FullName%, and %CurrentFolder.Parent%.
Understanding this object model matters enormously for dynamic path construction. When you need to build a destination path, you do it by combining properties:
%CurrentFile.Directory%\Processed\%CurrentFile.Name%
Or more commonly, using variables:
%OutputFolder%\%CurrentFile.NameWithoutExtension%_%Timestamp%.pdf
The file object also carries metadata that you'd otherwise need a separate system call to get. %CurrentFile.LastModified% lets you filter files older than a certain date without ever opening the file. %CurrentFile.Size% lets you skip zero-byte placeholder files that sometimes appear in network shares before the actual content is written.
Key insight
PAD's file and folder objects are lazy snapshots. They capture the state of the file system at the moment the "Get files" action ran. If a file is deleted or renamed by another process after that point, the object still holds the old data. In shared environments, always check file existence before operating on it.
The Get files in folder action is your primary entry point for file-based automation. Its configuration panel gives you several options worth understanding in depth:
*.pdf gets only PDFs. Invoice_*.xlsx gets Excel files whose names start with "Invoice_". You can only specify one filter pattern here, which is a common frustration.Here's a real problem: you need all .pdf AND .xlsx files from the incoming folder. The action only takes one filter. Two approaches:
Approach 1: Run Get Files twice, merge lists
Get files in folder:
Folder: %IncomingFolder%
Filter: *.pdf
→ Store in: PDFFiles
Get files in folder:
Folder: %IncomingFolder%
Filter: *.xlsx
→ Store in: ExcelFiles
# Combine them
Set variable: CombinedFiles = PDFFiles
For each ExcelFile in ExcelFiles:
Add item to list: Item = ExcelFile, List = CombinedFiles
Approach 2: Get all files, filter with conditions in the loop
Get files in folder:
Folder: %IncomingFolder%
Filter: *.*
→ Store in: AllFiles
For each CurrentFile in AllFiles:
If %CurrentFile.Extension% = '.pdf' OR %CurrentFile.Extension% = '.xlsx':
# Process this file
End if
Approach 1 keeps your list clean before the loop. Approach 2 is simpler but mixes filtering logic with processing logic. For complex pipelines with many file types, Approach 1 scales better and is easier to test in isolation.
A very common requirement: process only files modified in the last 24 hours. PAD doesn't expose a "modified after" parameter in Get Files, so you handle it in the loop:
Get current date and time → Store in: %Now%
Subtract from datetime: DateTime = %Now%, Hours = 24 → Store in: %Cutoff%
Get files in folder: Folder = %IncomingFolder%, Filter = *.pdf → Store in: %IncomingFiles%
For each CurrentFile in IncomingFiles:
If %CurrentFile.LastModified% >= %Cutoff%:
# File is recent, process it
End if
End for
Tip
When filtering by date on a network share, be aware of clock skew between the machine running your flow and the file server. A file written "now" on the server might appear to have a timestamp 3–5 seconds in the future from your bot's perspective. Add a small buffer — check for files modified in the last 25 hours rather than 24 when running daily.
"Include subfolders" on a network share with 50,000 files will block for 30–90 seconds before returning. This is because PAD enumerates the entire tree synchronously. Design considerations:
Incoming\2024\11\). Your bot only scans today's folder.The decision between copying and moving files is more significant than it appears. It encodes your process's failure semantics:
The Rename file(s) action supports several modes:
For a production naming pattern like VENDOR_YYYYMMDD_ORIGINALNAME.pdf, you need to build the name yourself and use "Set new name":
Get current date and time → Store in: %Now%
Format datetime: DateTime = %Now%, Format = 'yyyyMMdd' → Store in: %DateStamp%
For each CurrentFile in IncomingFiles:
# Extract vendor from filename (assume first segment before underscore)
Split text: Text = %CurrentFile.NameWithoutExtension%, Delimiter = '_'
→ Store in: %FileNameParts%
Set variable: VendorCode = %FileNameParts[0]%
Set variable: NewName = %VendorCode%_%DateStamp%_%CurrentFile.Name%
Rename file: File = %CurrentFile.FullName%,
Rename scheme = 'Set new name',
New name = %NewName%
→ Store renamed file in: %RenamedFile%
Warning
The Rename file action returns the renamed file object in its output variable. Crucially, your original %CurrentFile% variable now points to a file that no longer exists at its old path. If you need to continue operating on that file later in the loop, use the output variable from the Rename action — not %CurrentFile%. This is a very common source of "file not found" errors in complex loops.
Here's the pattern you'll use in almost every file processing flow:
# Phase 1: Copy to working folder
For each CurrentFile in IncomingFiles:
Copy file: File = %CurrentFile.FullName%,
Destination = %WorkingFolder%,
If file exists = Overwrite
→ Store in: %WorkingFile%
End for
# Phase 2: Process files in working folder
Get files in folder: Folder = %WorkingFolder%, Filter = *.*
→ Store in: %WorkingFiles%
For each WFile in WorkingFiles:
# Do your processing here
# (read data, transform, write output, etc.)
End for
# Phase 3: Move originals to archive (only after processing succeeds)
For each CurrentFile in IncomingFiles:
Move file: File = %CurrentFile.FullName%,
Destination = %ArchiveFolder%,
If file exists = Overwrite
End for
Separating these phases — copy, process, archive — means any failure in Phase 2 leaves your originals untouched in the incoming folder. Your next run picks them up again.
PAD's Delete file(s) action is permanent by default — it bypasses the Recycle Bin. For production flows, either:
%WINDIR%\system32\cmd.exe shell command with recycle.exe — though this requires administrative setupA recurring requirement: ensure a folder exists before writing files into it. The Create folder action fails if the folder already exists — unless you check first.
Get current date and time → Store in: %Now%
Format datetime: DateTime = %Now%, Format = 'yyyy' → Store in: %Year%
Format datetime: DateTime = %Now%, Format = 'MM' → Store in: %Month%
Format datetime: DateTime = %Now%, Format = 'MMMM' → Store in: %MonthName%
Set variable: YearFolder = %ArchiveRoot%\%Year%
Set variable: MonthFolder = %YearFolder%\%Month%_%MonthName%
# Create year folder if it doesn't exist
If not folder exists: Folder = %YearFolder%:
Create folder: Folder path = %YearFolder%
End if
# Create month folder if it doesn't exist
If not folder exists: Folder = %MonthFolder%:
Create folder: Folder path = %MonthFolder%
End if
Note
PAD's If folder exists and If not folder exists conditionals check at runtime. In multi-threaded or multi-bot environments where two flows might run simultaneously, there's a race condition: both check, both find the folder missing, both try to create it — and one fails. Wrap folder creation in an On Block Error handler that ignores "already exists" errors, or centralize folder provisioning in a flow that runs before workers start.
A maintenance subflow that you should build into any production archive system:
Get subfolders in folder: Folder = %ArchiveRoot%,
Include subfolders = No
→ Store in: %YearFolders%
Get current date and time → Store in: %Now%
Subtract from datetime: DateTime = %Now%, Days = 365 → Store in: %RetentionCutoff%
For each YearFolder in YearFolders:
Get subfolders in folder: Folder = %YearFolder.FullName%
→ Store in: %MonthFolders%
For each MonthFolder in MonthFolders:
Get files in folder: Folder = %MonthFolder.FullName%
→ Store in: %ArchivedFiles%
If %ArchivedFiles.Count% = 0:
# Empty folder, check if it's old enough to remove
If %MonthFolder.CreationTime% < %RetentionCutoff%:
Delete folder: Folder = %MonthFolder.FullName%
End if
End if
End for
End for
This pattern — walking a folder tree and applying age-based policies — is something you'll adapt for dozens of different retention and cleanup scenarios.
PAD gives you two distinct groups for email:
Outlook actions: Directly control an installed Outlook application via COM automation. Requires Outlook to be installed, licensed, and configured with an account. Very powerful — you can access folders, read headers, handle attachments, move messages, mark as read, and more.
Email (IMAP/POP3/SMTP) actions: Protocol-level access with explicit server credentials. More portable (works without Outlook), but limited to basic send/receive. Use this for server-side or unattended scenarios where Outlook can't be assumed.
For enterprise scenarios on Windows machines with licensed Outlook, the Outlook actions are almost always the right choice. For cross-platform or credential-driven scenarios (e.g., an unattended bot using a service account email), SMTP/IMAP is cleaner.
Key insight
Outlook COM automation requires Outlook to be in a running, responsive state. On an unattended machine, Outlook might not be open. The Launch Outlook action handles this — it opens Outlook if it isn't already running. But it won't succeed if Outlook is stuck on a dialog box (password prompt, update nag, profile corruption warning). Unattended email automation requires a properly configured, dialog-free Outlook profile. Consider using a dedicated service account profile with saved credentials and auto-save disabled. See Attended vs Unattended RPA: Choosing a Run Mode and Configuring Machines in Power Automate for machine configuration guidance.
A realistic scenario: every morning, vendors send invoices as email attachments. Your bot needs to download those attachments to the incoming folder.
# Step 1: Connect to Outlook
Launch Outlook → Store instance in: %OutlookInstance%
# Step 2: Get emails from inbox matching our criteria
Retrieve email messages from Outlook:
Account: vendor.processing@company.com
Mail folder: Inbox
Email status: Unread
Mark as read on retrieval: Yes
From contains: @vendor.com
Subject contains: Invoice
Retrieve: All messages
→ Store in: %VendorEmails%
# Step 3: Process each email
For each Email in VendorEmails:
# Check if email has attachments
If %Email.HasAttachments% = True:
# Save attachments to incoming folder
Save Outlook email message attachments:
Account: vendor.processing@company.com
Email message: %Email%
Attachment folder: %IncomingFolder%
→ Store attachment paths in: %SavedAttachments%
# Log what we received
Write text to file:
File: %LogFile%
Text: %Email.From% | %Email.Subject% | %Now% | %SavedAttachments.Count% attachments
# Move email to processed folder
Move Outlook email message:
Account: vendor.processing@company.com
Email message: %Email%
Mail folder: Processed\Invoices
End if
End for
When PAD retrieves email messages, each message object exposes:
%Email.From% → "Jane Smith <jane@vendor.com>"
%Email.To% → "vendor.processing@company.com"
%Email.CC% → ""
%Email.Subject% → "Invoice #4721 - November 2024"
%Email.Body% → Full plain-text body
%Email.HTMLBody% → HTML version of body
%Email.Date% → DateTime object
%Email.HasAttachments% → True/False
%Email.IsRead% → True/False
%Email.EntryId% → Outlook unique ID (use this to reference the email later)
Warning
%Email.From% returns the full formatted string including display name and angle brackets — "Jane Smith <jane@vendor.com>" — not just the email address. If you need to parse just the address for matching or logging, you'll need to use text manipulation: split on <, take the second part, strip the >. Build this into a reusable subflow rather than repeating it every time you need a clean email address.
Sending through Outlook is straightforward but has a few nuances for production use:
# Build a recipient list dynamically from a data table
Set variable: RecipientList = ''
For each Row in VendorContactTable:
Set variable: RecipientList = %RecipientList%;%Row['Email']%
End for
# Strip leading semicolon
Trim text: Text = %RecipientList%, What to trim = Leading characters
→ Store in: %RecipientList%
# Build the email body with dynamic content
Set variable: EmailBody =
'Dear Team,
The following invoices have been processed for %MonthName% %Year%:
- Total files processed: %ProcessedCount%
- Total files failed: %FailedCount%
- Archive location: %MonthFolder%
Please review the processing log at %LogFile% for details.
This is an automated message from the Invoice Processing Bot.'
# Send via Outlook
Send email through Outlook:
Account: vendor.processing@company.com
To: %RecipientList%
CC: finance.manager@company.com
Subject: Invoice Processing Complete - %MonthName% %Year%
Body: %EmailBody%
Body is HTML: No
Attachments: %LogFile%
For HTML emails, build your body string as valid HTML. PAD doesn't validate the HTML — it passes whatever string you provide directly to Outlook's HTML body property. This means you can use full HTML with inline styles:
Set variable: HTMLBody =
'<!DOCTYPE html>
<html>
<body style="font-family: Calibri, sans-serif; font-size: 14px;">
<h2 style="color: #1F4E79;">Invoice Processing Summary</h2>
<table border="1" cellpadding="6" style="border-collapse: collapse; width: 100%;">
<tr style="background-color: #BDD7EE;">
<th>Vendor</th><th>File</th><th>Status</th>
</tr>'
For each Row in ProcessingResults:
Set variable: RowColor = '#FFFFFF'
If %Row['Status']% = 'Failed':
Set variable: RowColor = '#FFE0E0'
End if
Set variable: HTMLBody = %HTMLBody% &
'<tr style="background-color: ' & %RowColor% & ';">
<td>' & %Row['Vendor']% & '</td>
<td>' & %Row['FileName']% & '</td>
<td>' & %Row['Status']% & '</td>
</tr>'
End for
Set variable: HTMLBody = %HTMLBody% & '</table></body></html>'
Send email through Outlook:
Body: %HTMLBody%
Body is HTML: Yes
This pattern — building a processing results data table throughout your flow and then rendering it as HTML at the end — produces professional-looking status emails that operations teams actually find useful.
Tip
Avoid using %Tab% or %NewLine% special variables inside HTML body strings. HTML ignores whitespace anyway. But be careful with quote characters inside your HTML — single quotes inside double-quoted variable values (or vice versa) are fine, but if you need both, use HTML entities: ' for single quote, " for double quote.
Let's build the full pipeline. This is a realistic scenario that combines everything we've covered. We'll structure it as a main flow that calls subflows — a pattern discussed in depth in Subflows and Reusable Logic in Power Automate Desktop.
Main Flow
├── Subflow: Initialize (set variables, create folders, connect Outlook)
├── Subflow: DownloadEmailAttachments (fetch from inbox, save to Incoming)
├── Subflow: ProcessFiles (rename, validate, copy to working folder)
├── Subflow: ArchiveAndCleanup (move originals, delete working copies)
└── Subflow: SendStatusReport (build HTML report, email stakeholders)
# --- Subflow: Initialize ---
# Base paths from environment (set as input variables from cloud flow trigger)
# See: Triggering Desktop Flows from Cloud Flows
Set variable: BaseFolder = %InputBaseFolder%
Set variable: IncomingFolder = %BaseFolder%\Incoming
Set variable: WorkingFolder = %BaseFolder%\Working
Set variable: ArchiveFolder = %BaseFolder%\Archive
Set variable: LogFolder = %BaseFolder%\Logs
# Ensure all required folders exist
For each FolderPath in ['%IncomingFolder%', '%WorkingFolder%', '%LogFolder%']:
If not folder exists: Folder = %FolderPath%:
Create folder: Folder path = %FolderPath%
End if
End for
# Create today's archive folder
Get current date and time → Store in: %Now%
Format datetime: DateTime = %Now%, Format = 'yyyy\\MM_MMMM' → Store in: %ArchiveSuffix%
Set variable: TodayArchive = %ArchiveFolder%\%ArchiveSuffix%
If not folder exists: Folder = %TodayArchive%:
Create folder: Folder path = %TodayArchive%
End if
# Set up log file for this run
Format datetime: DateTime = %Now%, Format = 'yyyyMMdd_HHmmss' → Store in: %RunStamp%
Set variable: LogFile = %LogFolder%\ProcessingLog_%RunStamp%.txt
Write text to file: File = %LogFile%,
Text = 'Invoice Processing Started: %Now%',
Append = False
# Initialize tracking variables
Set variable: ProcessedCount = 0
Set variable: FailedCount = 0
Create new data table: → Store in: %ProcessingResults%
Columns: ['Vendor', 'FileName', 'Status', 'Notes']
# Connect to Outlook
Launch Outlook → Store in: %OutlookInstance%
# --- Subflow: DownloadEmailAttachments ---
On block error:
Action: Continue flow
Write text to file: File = %LogFile%,
Text = 'ERROR in DownloadEmailAttachments: %LastError%',
Append = True
End on block error
Retrieve email messages from Outlook:
Account: %OutlookAccount%
Mail folder: Inbox
Email status: Unread
Subject contains: Invoice
Retrieve: All messages
→ Store in: %InvoiceEmails%
Write text to file: File = %LogFile%,
Text = 'Found %InvoiceEmails.Count% invoice emails',
Append = True
For each Email in InvoiceEmails:
If %Email.HasAttachments% = True:
On block error:
Action: Continue flow
Write text to file: File = %LogFile%,
Text = 'Failed to save attachments from: %Email.From%',
Append = True
End on block error
Save Outlook email message attachments:
Account: %OutlookAccount%
Email message: %Email%
Attachment folder: %IncomingFolder%
→ Store in: %SavedAttachments%
Write text to file: File = %LogFile%,
Text = 'Saved %SavedAttachments.Count% attachments from %Email.From%',
Append = True
Move Outlook email message:
Account: %OutlookAccount%
Email message: %Email%
Mail folder: Processed\Invoices
Else:
Write text to file: File = %LogFile%,
Text = 'WARNING: Email from %Email.From% has no attachments - Subject: %Email.Subject%',
Append = True
End if
End for
# --- Subflow: ProcessFiles ---
Get files in folder: Folder = %IncomingFolder%, Filter = *.*
→ Store in: %IncomingFiles%
Write text to file: File = %LogFile%,
Text = 'Processing %IncomingFiles.Count% files',
Append = True
For each CurrentFile in IncomingFiles:
# Skip zero-byte files (incomplete uploads)
If %CurrentFile.Size% = 0:
Write text to file: File = %LogFile%,
Text = 'SKIP: Zero-byte file %CurrentFile.Name%',
Append = True
Continue
End if
# Only process PDF and Excel
If %CurrentFile.Extension% <> '.pdf' AND %CurrentFile.Extension% <> '.xlsx':
Write text to file: File = %LogFile%,
Text = 'SKIP: Unsupported extension %CurrentFile.Name%',
Append = True
Continue
End if
On block error:
Action: Continue flow
Set variable: FailedCount = %FailedCount% + 1
Add row to data table:
DataTable = %ProcessingResults%
Row: ['Unknown', '%CurrentFile.Name%', 'Failed', '%LastError%']
Write text to file: File = %LogFile%,
Text = 'FAILED: %CurrentFile.Name% - %LastError%',
Append = True
End on block error
# Parse vendor from filename (format: VENDORCODE_anything.ext)
Split text: Text = %CurrentFile.NameWithoutExtension%, Delimiter = '_'
→ Store in: %NameParts%
Set variable: VendorCode = %NameParts[0]%
# Build standardized name
Format datetime: DateTime = %Now%, Format = 'yyyyMMdd' → Store in: %DateStamp%
Set variable: StandardName = %VendorCode%_%DateStamp%_%CurrentFile.Name%
# Copy to working folder with standard name
Copy file: File = %CurrentFile.FullName%,
Destination = %WorkingFolder%\%StandardName%,
If file exists = Overwrite
# Track success
Set variable: ProcessedCount = %ProcessedCount% + 1
Add row to data table:
DataTable = %ProcessingResults%
Row: ['%VendorCode%', '%StandardName%', 'Processed', '']
# Move original to today's archive
Move file: File = %CurrentFile.FullName%,
Destination = %TodayArchive%\%CurrentFile.Name%,
If file exists = Overwrite
End for
# --- Subflow: SendStatusReport ---
# Build HTML status table from ProcessingResults data table
Set variable: TableRows = ''
For each Row in ProcessingResults:
Set variable: StatusColor = '#E2EFDA' # Green for success
If %Row['Status']% = 'Failed':
Set variable: StatusColor = '#FCE4D6' # Orange for failure
End if
Set variable: TableRows = %TableRows% &
'<tr style="background:' & %StatusColor% & ';">' &
'<td style="padding:6px;border:1px solid #ccc;">' & %Row['Vendor']% & '</td>' &
'<td style="padding:6px;border:1px solid #ccc;">' & %Row['FileName']% & '</td>' &
'<td style="padding:6px;border:1px solid #ccc;font-weight:bold;">' & %Row['Status']% & '</td>' &
'<td style="padding:6px;border:1px solid #ccc;">' & %Row['Notes']% & '</td>' &
'</tr>'
End for
Set variable: HTMLReport =
'<html><body style="font-family:Calibri,sans-serif;">
<h2 style="color:#1F4E79;">Invoice Processing Report</h2>
<p><strong>Run time:</strong> ' & %Now% & '</p>
<p><strong>Processed:</strong> ' & %ProcessedCount% & '
<strong>Failed:</strong> ' & %FailedCount% & '</p>
<table style="border-collapse:collapse;width:100%">
<tr style="background:#2E75B6;color:white;">
<th style="padding:8px;">Vendor</th>
<th style="padding:8px;">File</th>
<th style="padding:8px;">Status</th>
<th style="padding:8px;">Notes</th>
</tr>' & %TableRows% & '</table>
<p style="color:#888;font-size:12px;">Automated message from Invoice Processing Bot</p>
</body></html>'
Send email through Outlook:
Account: %OutlookAccount%
To: %NotificationRecipients%
Subject: Invoice Processing Complete: %ProcessedCount% processed, %FailedCount% failed - %DateStamp%
Body: %HTMLReport%
Body is HTML: Yes
Attachments: %LogFile%
Build the following flow from scratch. This will take approximately 60–90 minutes and covers all the major patterns from this lesson.
Scenario: A shared folder (C:\Reports\Incoming) receives CSV export files from three different systems each morning. Files are named in formats like CRM_export_20241115.csv, ERP_daily_20241115.csv, and WH_stock_20241115.csv. Your flow should:
.csv files from the incoming folderC:\Reports\Archive named YYYY-MM-DD for today's date if it doesn't already existC:\Reports\ProcessedC:\Reports\LogsExtension challenge: Modify the flow to handle the case where the incoming folder doesn't exist yet (perhaps the upstream system hasn't run). Instead of failing, log a warning and send a different email with subject "WARNING: No incoming folder found" and skip all file processing.
Symptom: Your loop renames a file, then immediately tries to move or read it — and gets a "file not found" error.
Cause: You're using %CurrentFile.FullName% which still holds the pre-rename path. After a rename, use the output variable of the Rename action.
Fix: Always capture the rename output: Rename file → Store in: %RenamedFile%, then operate on %RenamedFile.FullName%.
Symptom: The Retrieve emails action fails intermittently with a COM exception or "item could not be opened."
Cause: Usually a profile or connectivity issue. Can also be a corrupted OST file or a cached message that Outlook can't render.
Fix: Wrap in an On Block Error with retry (3 attempts, 30-second delay). For persistent failures, the Outlook profile on the machine needs attention. On unattended bots, this almost always means the machine profile hasn't been configured to auto-connect to Exchange without prompting.
Symptom: Some files aren't getting processed, but you see no errors.
Cause: Usually a filter mismatch. Extension check using %CurrentFile.Extension% on a file named Invoice.PDF (uppercase) returns .PDF — and your comparison is against .pdf. PAD string comparisons in conditions are case-sensitive.
Fix: Normalize extensions to lowercase before comparing:
Convert text to lowercase: Text = %CurrentFile.Extension% → Store in: %ExtLower%
If %ExtLower% = '.pdf' OR %ExtLower% = '.xlsx':
Symptom: Move file fails because the destination path doesn't exist — even though you thought you created it.
Cause: Your folder creation used a path built from variables, but a variable had a trailing space or included an invalid character.
Fix: Before any move or copy, check folder existence explicitly. Also log the exact destination path string to your log file so you can inspect it. Path debugging is much easier when you can see the actual string the action received.
Symptom: Two emails from the same vendor both have an attachment named invoice.pdf. The second one overwrites the first.
Cause: The Save Outlook email message attachments action names files by their original attachment name. If two emails have identically-named attachments, the second one overwrites the first in the destination folder.
Fix: After saving, rename each attachment to include a unique identifier. Use %Email.EntryId% (the Outlook unique message ID) as part of the new name, or use a sequence counter. Better yet, save to a message-specific subfolder:
Set variable: EmailSubfolder = %IncomingFolder%\%Email.EntryId%
Create folder: Folder path = %EmailSubfolder%
Save attachments: Attachment folder = %EmailSubfolder%
Warning
File system paths on Windows have a 260-character limit by default (the MAX_PATH limit). If %Email.EntryId% is long (and Outlook entry IDs can be very long hex strings), your subfolder path might exceed 260 characters, causing mysterious failures. Use the first 20 characters of the entry ID: Substring of text: Text = %Email.EntryId%, Start = 0, Count = 20.
Symptom: Processing 200 files takes much longer than expected.
Cause: Each file operation — copy, move, rename — is an individual OS call that goes through PAD's action engine. On network paths, latency per operation compounds significantly.
Fix:
You've now built a deep, nuanced understanding of how Power Automate Desktop handles file system and email operations at a production level. The core ideas to carry forward:
.FullName, .Extension, .LastModified) directly rather than parsing path strings manually.For your next steps, consider how this pipeline integrates with the wider automation ecosystem. The files your bot downloads and processes might need to be read into Excel for further data manipulation. If some of those invoice PDFs are scanned, you'll want to combine this pipeline with OCR text extraction. And once your flow is solid, triggering it from a cloud flow — so it runs automatically when a SharePoint folder receives a file or when an email arrives — is the natural evolution. That integration is covered in Triggering Desktop Flows from Cloud Flows: Passing Inputs and Returning Outputs.
Finally, make sure your flow handles its credentials safely. The Outlook account details, any SMTP passwords, and shared folder credentials used in your flow should not be hardcoded as plain-text variables. See Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault for the right approach before you deploy this to production.