Learn how to build a single Power Automate Desktop flow that extracts data from a Windows application, enriches it via browser lookups, and writes the results to Excel — with proper synchronization, error handling, and subflow architecture. This is the multi-application orchestration pattern that separates production-ready RPA from hobby scripts.

Here's a workflow that plays out in countless organizations every day: a sales operations analyst opens a legacy order management system, pulls the day's new orders, cross-references each one against a pricing tool on an internal web portal, and then pastes the enriched data into an Excel workbook that feeds a Power BI dashboard. The whole process takes two hours. It requires the analyst to context-switch between three applications without losing their place, manually copy values between screens, and remember which rows they've already processed. One distraction — a Slack message, a phone call — and suddenly an order gets duplicated or a price gets applied to the wrong line.
This is exactly the kind of workflow that Power Automate Desktop was built to eliminate. Not because the task is intellectually complex, but because it's repetitive, multi-application, and utterly unforgiving of human error. By the end of this lesson, you'll know how to design a single desktop flow that opens and reads data from a Windows application, navigates a web browser to look up additional information, combines those results in memory, and writes the final output to Excel — all without you touching the keyboard.
What you'll learn:
This lesson is pitched at practitioners. You should already be comfortable with:
If you need a refresher on any of those, Variables, Lists, and Data Tables in Power Automate Desktop is the right starting point. We'll also touch on browser automation, and Web Automation in Power Automate Desktop: Browser Actions, Form Filling, and Data Extraction covers that territory in depth.
Throughout this lesson, we'll work on a realistic automation: a nightly Order Enrichment Flow for a wholesale distributor.
The flow needs to:
The output file is picked up by a Power BI refresh job at 6:00 AM. The whole flow needs to be reliable enough to run unattended.
This scenario is deliberately representative. Swap "order management app" for "SAP GUI" or "Citrix-hosted ERP" and "pricing portal" for any internal web tool, and the architecture stays exactly the same.
Before touching the flow editor, spend five minutes sketching the data lifecycle. Multi-application flows fail most often because developers jump straight into recording actions without thinking about how data moves between applications.
Your flow has three distinct stages:
Stage 1 — Extract: Read all the orders from the Windows app into a variable that survives after you close or minimize the app.
Stage 2 — Enrich: Loop through each record, open the browser to fetch the current price, and update the record in memory.
Stage 3 — Load: Open Excel, navigate to the right sheet, and write every enriched record.
The variable that bridges Stage 1 and Stage 2 is a Data Table. Power Automate Desktop's DataTable type is perfect here — it holds multiple rows and named columns, it's easy to iterate, and you can add columns to it dynamically as you enrich each row.
Key insight
Think of your data table as a staging area in RAM. Every application you touch either puts data into that table or reads data from it. The table itself is application-agnostic, which is what makes the whole orchestration possible.
The variable that bridges Stage 2 and Stage 3 is the same table — now with additional columns populated. You write it to Excel once, at the end, rather than opening and closing Excel in the middle of the loop.
This "Extract → Transform → Load" mental model maps directly to how you'll structure your flow's subflows.
Start with a Launch Application action pointing to the order management executable. Capture the resulting window handle in a variable called OrderAppWindow.
Action: Launch Application
Application path: C:\Program Files\OrderMgmt\OrderMgmt.exe
Arguments: --mode=today
Window style: Normal
Store result in: OrderAppWindow
After launching, add a Wait for Window action (not just a fixed delay) to ensure the app has fully loaded before you start interacting with it. Hardcoded Wait actions with fixed seconds are a reliability trap — the app might load in 2 seconds on a fast machine and 8 seconds after a Windows update installs overnight.
Action: Wait for Window
Window title contains: Order Management - Today's View
Timeout: 30 seconds
In our scenario, today's orders are displayed in a grid or list view inside the Windows app. If the app is a standard Windows Forms or WPF application, Power Automate Desktop's UI Automation actions can extract the entire table contents using Get Details of UI Element in Window or Extract Data from Window.
For a proper data grid, use the Get Rows from a Window Data Grid approach:
Action: Get rows from a data grid
Window: OrderAppWindow
Data grid UI element: [captured selector pointing to the grid control]
Store result as: RawOrdersTable
The result is a DataTable with columns like OrderID, CustomerName, SKU, Quantity, OrderDate.
Warning
Not every grid exposes its contents to UI Automation this cleanly. If your app uses a custom-drawn grid or a legacy ActiveX control, you may need to use clipboard extraction — selecting all rows with Ctrl+A, copying with Ctrl+C, and then parsing the clipboard text. Check the UI tree with the Inspect tool before assuming the clean path will work. For difficult legacy apps, Automating Legacy Windows Applications with UI Automation in Power Automate Desktop walks through the fallback strategies.
After extracting, add a column to the table for the data you'll populate in Stage 2:
Action: Add column to data table
Table: RawOrdersTable
Column name: UnitPrice
Default value: 0
Action: Add column to data table
Table: RawOrdersTable
Column name: LineTotal
Default value: 0
Action: Add column to data table
Table: RawOrdersTable
Column name: DiscountPct
Default value: 0
Now minimize the order management app. You don't need it anymore, but closing it might trigger a logout or session reset you don't want.
This is where the flow's main loop lives. You'll iterate over every row in RawOrdersTable, go to the pricing portal for that row's SKU, capture the price, do the math, and update the row.
Action: For each row in data table
Data table: RawOrdersTable
Loop variable: CurrentOrder
Inside that loop, your first task is to extract the SKU and Quantity from the current row:
Action: Set variable
Name: CurrentSKU
Value: %CurrentOrder['SKU']%
Action: Set variable
Name: CurrentQty
Value: %CurrentOrder['Quantity']%
If the browser isn't open yet, launch it. A pattern that works well in production is to launch the browser before the loop starts and reuse it across all iterations, rather than opening and closing it per row. Launching a browser has a noticeable startup cost, and at 200 orders per run that overhead compounds fast.
Before the loop:
Action: Launch new Microsoft Edge
URL: https://pricing.internal.contoso.com
Window state: Normal
Store result as: PricingBrowser
Inside the loop, navigate to the SKU-specific URL:
Action: Go to web page
Browser instance: PricingBrowser
URL: https://pricing.internal.contoso.com/lookup?sku=%CurrentSKU%
Tip
Many internal pricing portals use query string parameters for lookups, which makes URL navigation trivially easy. If yours requires form submission instead, use the Populate text field on web page and Click link on web page actions to drive the search. The selector skills you need for that are covered thoroughly in UI Elements and Selectors in Power Automate Desktop: Building Automations That Don't Break.
After navigation, wait for the page to contain the result element rather than using a fixed delay:
Action: Wait for web page content
Browser instance: PricingBrowser
Check that: Element exists on page
UI element: [selector for the price display element]
Timeout: 15 seconds
Action: Get detail of element on web page
Browser instance: PricingBrowser
UI element: [selector for the unit price span/div]
Attribute: Own text
Store result as: RawPrice
RawPrice will likely come back as a string like "$47.50". You need a number:
Action: Replace text
Text to search: RawPrice
Text to find: $
Replace with: (empty)
Store result as: CleanPrice
Action: Convert text to number
Text: %CleanPrice%
Store result as: UnitPrice
Now that you have both UnitPrice and CurrentQty as numbers, the math is straightforward:
Action: Set variable
Name: LineTotal
Value: %UnitPrice * CurrentQty%
Apply a tiered discount — let's say orders over 100 units get 10% off, orders over 500 units get 18% off:
Action: If
First operand: %CurrentQty%
Operator: Greater than or equal to
Second operand: 500
Action: Set variable
Name: DiscountPct
Value: 18
Else if
First operand: %CurrentQty%
Operator: Greater than or equal to
Second operand: 100
Action: Set variable
Name: DiscountPct
Value: 10
Else
Action: Set variable
Name: DiscountPct
Value: 0
End
This step is where many beginners get stuck. You have a CurrentOrder loop variable that represents the current row, but modifying CurrentOrder doesn't automatically update the underlying table. You need to write back to the table using the row index.
The For each row action also exposes a loop index variable (typically LoopIndex starting at 0). Use it to update the source table directly:
Action: Set item in data table
Data table: RawOrdersTable
Row index: %LoopIndex%
Column name: UnitPrice
Value: %UnitPrice%
Action: Set item in data table
Data table: RawOrdersTable
Row index: %LoopIndex%
Column name: LineTotal
Value: %LineTotal%
Action: Set item in data table
Data table: RawOrdersTable
Row index: %LoopIndex%
Column name: DiscountPct
Value: %DiscountPct%
Note
Power Automate Desktop's For each row action provides a zero-based index counter. Make sure you're using the correct variable name — it defaults to LoopIndex but can be renamed. Confirm this in the action's properties before relying on it for table writes.
After the loop completes, close the browser instance:
Action: Close web browser
Browser instance: PricingBrowser
The target Excel file lives on a network share at \\fileserver01\reports\OrderEnrichment.xlsx. Open it using the Excel actions group:
Action: Launch Excel
File path: \\fileserver01\reports\OrderEnrichment.xlsx
Visible: False
Store Excel instance as: ExcelInstance
Running Excel invisible (background) is appropriate here because you're just writing data programmatically — there's no reason to expose the UI. This is faster and avoids distracting the user if the flow runs attended.
Warning
Running Excel as invisible means you won't see error dialogs. If Excel shows a "file is locked" dialog or a macro warning, your flow will hang indefinitely because there's no visible UI to respond to. Add an error handler around the Excel launch to catch this gracefully and either retry or notify.
Navigate to the correct worksheet. In this scenario, the workbook has a sheet called DailyOrders that gets cleared and repopulated each run:
Action: Get active worksheet
Excel instance: ExcelInstance
Store result as: ActiveSheet
Action: Set active Excel worksheet
Excel instance: ExcelInstance
Activate worksheet with name: DailyOrders
Clear the existing data below the header row so you start fresh:
Action: Get first free row on column from Excel worksheet
Excel instance: ExcelInstance
Column: A
Store first free row as: LastUsedRow
Action: If
First operand: %LastUsedRow%
Operator: Greater than
Second operand: 2
Action: Delete row from Excel worksheet
Excel instance: ExcelInstance
Delete row: 2
Row count: %LastUsedRow - 2%
End
The cleanest approach is to write the entire enriched data table to the sheet starting at row 2 (row 1 is the header):
Action: Write to Excel worksheet
Excel instance: ExcelInstance
Value to write: %RawOrdersTable%
Write mode: On specified cell
Start column: A
Start row: 2
Power Automate Desktop's Write to Excel worksheet action accepts a DataTable as input and writes it as a contiguous block — no loop required. This is significantly faster than writing row by row, especially for large datasets.
For more control over named ranges, column formatting, and sheet management without macros, see Working with Excel Ranges, Sheets, and Named Cells in Power Automate Desktop.
Action: Save Excel
Excel instance: ExcelInstance
Save mode: Save document (overwrite existing file)
Action: Close Excel
Excel instance: ExcelInstance
Before closing: Do not save document
Note the two-step approach: save explicitly first, then close without saving again. This avoids the "do you want to save?" dialog that appears when Excel closes an unsaved workbook.
At this point, if you've been building linearly, you have somewhere between 60 and 90 actions in a single flat flow. That's hard to read, hard to debug, and genuinely painful to hand off to a colleague.
Break the flow into subflows that map to your three stages:
Main — the orchestrator; calls the others in sequenceExtractOrders — all Stage 1 logic; returns RawOrdersTableEnrichOrders — all Stage 2 logic; takes and updates RawOrdersTableWriteToExcel — all Stage 3 logic; takes RawOrdersTable and writes itUsing subflows this way also lets you test each stage independently, which cuts debugging time substantially. If the Excel write is failing, you don't have to sit through the 10-minute extraction and browser loop to retest it — you can mock the table and call WriteToExcel directly.
Subflows and Reusable Logic in Power Automate Desktop covers the mechanics of passing variables between subflows, which is non-obvious the first time you do it (output variables must be explicitly declared).
Multi-application flows have more timing-related failure modes than single-app flows, because each application transition is a potential synchronization gap.
When your flow switches between applications — say, from the web browser back to Excel — the operating system needs a moment to shift focus. Actions that fire too quickly after a window receives focus can land on the wrong element or produce no output.
Use Focus Window before interacting with any app after a switch:
Action: Focus window
Window: ExcelInstance
And pair it with a Wait for UI element action rather than a static Wait to confirm the target element is ready:
Action: Wait for UI element on window
UI element: [first editable cell on DailyOrders sheet]
Wait for element to: Exist
Timeout: 10 seconds
Not every SKU in the order system will have a current price on the portal. A discontinued item, a new product pending setup, or a portal outage can all result in the price element not appearing. Without handling this, your flow crashes on the first missing SKU and writes nothing to Excel.
Wrap the browser extraction block in an On block error handler:
On block error
Behavior: Continue flow execution
Action: Set variable
Name: UnitPrice
Value: 0
Action: Set variable
Name: LookupError
Value: True
End
Then, after the loop, you can log which SKUs had lookup errors to a separate Excel sheet for manual review — a much better outcome than a crashed flow and no output file.
Key insight
In multi-application flows, partial success is almost always better than total failure. Design your error handling so that a single bad row doesn't abort the entire run. Write the rows you could enrich, flag the ones you couldn't, and let a human handle the exceptions. This is the difference between a flow that's genuinely useful in production and one that requires babysitting.
If your flow crashes mid-run and restarts, the order management app and the browser may already be open. Launching them again creates duplicate instances, and your selectors may hit the wrong one.
Add a Get open windows action at the start of the flow and check whether your target apps are already running:
Action: Get open windows
Filter by window title: Order Management
Store result as: ExistingWindows
Action: If
First operand: %ExistingWindows.Count%
Operator: Greater than
Second operand: 0
Action: Focus window
Window title: Order Management
Else
Action: Launch Application
...
End
This "idempotent startup" pattern makes your flow safe to restart without manual cleanup.
Build the Order Enrichment Flow end-to-end using the following simplified scenario, which you can run without a real legacy app or internal portal.
Setup:
C:\PAD_Exercise\orders.csv with columns: OrderID, SKU, Quantity. Add 5 rows of sample data.C:\PAD_Exercise\enriched_orders.xlsx.Your flow should:
https://httpbin.org/get?sku=[SKU] and extract the args.sku value from the JSON response displayed in the browser. Use this as your "looked up" value (standing in for a real price).This exercise forces you to practice reading from one data source (a file), enriching via browser, and writing to another (Excel) — the same pattern as the production scenario, with no enterprise systems required.
Mistake: Writing back to CurrentOrder instead of the source table
In a For each row loop, CurrentOrder is a copy of the row, not a reference to it. Modifying CurrentOrder does nothing to RawOrdersTable. Always use Set item in data table with the loop index to update the source.
Mistake: Opening and closing the browser on every loop iteration
This is the single biggest performance killer in browser-heavy flows. Launch the browser once before the loop, reuse the instance inside, and close it after. On a 300-row dataset, this can mean the difference between a 45-minute run and a 6-minute run.
Mistake: Using fixed Wait actions instead of element-based waits
A Wait 3 seconds action that works on your development machine may fail on a slower unattended machine or during peak load. Replace every fixed wait with Wait for UI element or Wait for web page content. Your flow becomes environment-independent.
Mistake: Not handling Excel file lock errors
If another user has the output Excel file open when your flow tries to write to it, Excel throws a sharing violation. Detect this by checking the ExcelInstance variable after launch — if it's empty, the open failed. Send a notification and abort cleanly rather than hanging.
Mistake: Forgetting to write headers to Excel
When you write a DataTable starting at row 2, row 1 (the header) needs to already exist in the file. If you're generating the Excel file fresh each run, either write the headers as a first separate step, or maintain a template file with headers pre-set and copy it before the flow writes to it.
Tip
Keep a _template.xlsx file alongside your output file. At the start of each run, copy the template over the output file before opening it. This guarantees your headers are always there and your column formatting is consistent — no matter what happened on the previous run.
Mistake: Hardcoding application paths and URLs
If the order management app moves to a different server or the pricing portal URL changes, hardcoded values break silently or loudly at the worst times. Store these as flow input variables or — better — in a config sheet in your Excel workbook that the flow reads at startup. This makes updates a data change, not a flow change.
Troubleshooting: Flow hangs on browser interaction after switching from Windows app
This usually means the browser didn't fully receive focus before the action fired. Add a Focus window action targeting the browser, followed by a 500ms Wait (just this once — focus transitions are one of the few legitimate uses of a small fixed delay), and then your element-based wait.
Troubleshooting: DataTable writes wrong values to Excel columns
Power Automate Desktop writes DataTable columns to Excel in the order they appear in the table, starting from the left. If you added columns to the table dynamically (as we did with UnitPrice, LineTotal, and DiscountPct), they appear after the original columns. Make sure your Excel template's column order matches the DataTable column order exactly.
You've built a complete multi-application automation that does what a human takes two hours to do — and does it without errors, without distraction, and repeatable on demand. The architecture you've learned here — extract into a DataTable, enrich via loop, load to Excel — is genuinely universal. It applies whether your source is a Windows forms app, a Citrix-hosted legacy system, or even a SAP GUI transaction screen.
The key principles to carry forward:
Where to go next:
The automation you built today is a foundation, not a ceiling. Once it's running reliably, you can extend it: add an email notification when it completes, plug in OCR to handle orders that arrive as scanned PDFs, or promote it to an unattended bot that runs at 3 AM. Each of those is a layer on top of the same core pattern you now understand.
Power Automate Desktop & RPA
Working with Excel Ranges, Sheets, and Named Cells in Power Automate Desktop: Reading Tables, Writing Data, and Switching Worksheets Without Macros
Automating Image-Based UI Interactions in Power Automate Desktop: Using Screen Scraping, Image Recognition, and Coordinate-Based Actions When Selectors Fail