Learn how to build a production-grade unattended flow that scrapes dynamic tables across multiple browser tabs, normalizes multi-schema data in memory, and writes everything to Excel in a single synchronized pass. This expert-level lesson covers tab context management, sentinel-based dynamic load detection, schema normalization, and Excel bulk writeback — with full error isolation so partial failures never produce zero output.

Imagine you're a data engineer responsible for consolidating daily inventory figures from three separate supplier portals — each running in its own browser tab, each presenting a dynamically rendered table that loads its rows through JavaScript, and each updating on a different schedule throughout the day. Your manager wants a single Excel workbook refreshed every morning before anyone arrives at the office. No manual copy-paste. No human in the loop. Just a clean, up-to-date report waiting when they open their laptop.
That scenario captures exactly the class of problem this lesson addresses. Multi-tab browser scraping sounds straightforward until you hit the real friction: Power Automate Desktop's browser automation doesn't automatically track which tab is "active" in any meaningful way. When you open a second tab and extract from it, there's nothing preventing the runtime from accidentally firing actions against the first tab — or losing the browser instance entirely after a navigation event. Add a dynamically loaded table (the kind that appears only after an AJAX call completes), and you have a timing problem layered on top of a context problem. Now route all that extracted data into a single synchronized write to Excel, and you have a full-scale engineering challenge.
By the end of this lesson, you'll have built a complete, production-grade unattended flow that opens multiple browser tabs, extracts data from dynamic tables in each one, correctly switches tab context between extractions, accumulates all records into a unified data table, and writes everything to Excel in a single writeback pass — with proper error handling and logging throughout.
What you'll learn:
This lesson assumes expert-level comfort with Power Automate Desktop. You should already be familiar with:
You'll need Power Automate Desktop installed and licensed for unattended runs, a machine configured as an unattended bot host, and access to at least two web applications that render data in HTML tables.
Before writing a single action, you need a precise mental model of how Power Automate Desktop thinks about browser sessions, windows, and tabs. Getting this wrong is the root cause of nearly every multi-tab scraping bug.
When you use the Launch new Microsoft Edge (or Chrome) action, PAD returns a Browser instance variable — let's call it BrowserInstance. This variable is a reference to the browser process, not to any specific tab. Every subsequent browser action that accepts a Browser parameter uses that instance. Here's the critical insight: PAD's browser automation always targets the currently focused tab within that browser instance. It has no native concept of "tab index 2" or "the tab at this URL." It only knows "the tab that is currently in the foreground of this browser window."
This means that if your flow opens Tab 1, navigates somewhere, then opens Tab 2 (which becomes focused by default), every browser action after that point will fire against Tab 2 — even if you haven't explicitly told it to. If a background timer or another process clicks on Tab 1, your next extraction will silently pull from the wrong tab.
The practical consequence: in a multi-tab flow, you must explicitly assert focus on the correct tab before every block of browser actions. You can't assume focus is maintained between actions. This is especially true for unattended flows, where the Windows desktop may be processing other system events.
Key insight
PAD's browser actions are always context-bound to the foreground tab of the given browser instance. Tab switching is not implicit — it must be driven by your flow logic using explicit navigation or tab-focus actions. Never assume the tab that was focused three actions ago is still focused now.
There are two reliable strategies for asserting tab context:
Strategy 1: One browser instance per tab. Launch a separate Edge/Chrome process for each site. This gives you complete isolation — BrowserInstance1 always points to Site A, BrowserInstance2 always points to Site B. The drawback is resource overhead (three browser processes instead of one) and the fact that some SSO-authenticated sites share session cookies across processes, which can cause unexpected re-authentication prompts.
Strategy 2: Navigate to a URL to re-assert context. Before each extraction block, use Go to web page with the target URL and your BrowserInstance. This forces the browser to navigate the active tab to the right location and implicitly focuses that tab. If you've opened multiple tabs, you'll need to combine this with Switch to Browser Tab (available via the Get details of web page and related actions in some PAD versions) or use keyboard shortcuts through Send keys to cycle tabs.
For this lesson, we'll use Strategy 1 — separate browser instances per site — because it produces the most readable, maintainable flow logic and eliminates an entire category of tab-focus bugs. We'll discuss the trade-offs of Strategy 2 at the end.
Good architecture for this problem separates concerns into three distinct phases:
This three-phase design gives you several advantages. Each extraction subflow is independently testable. The merge logic is centralized, so schema changes only need updating in one place. And the single-pass writeback means you never end up with half-written Excel files if one source fails.
Let's talk about the data model first. Our scenario involves three supplier portals:
[SKU, ProductName, StockQty, UnitCost, LastUpdated][ItemCode, Description, Available, Price, Timestamp][PartNumber, Name, Inventory, Cost, Updated]The column names differ, but the semantic content is the same. Our unified schema will be:
[Source, SKU, ProductName, StockQty, UnitCost, LastUpdated]
Each row extracted from any supplier will be mapped into this schema before being added to the accumulator table.
Note
Defining a canonical schema upfront is not optional — it's the difference between a maintainable flow and a debugging nightmare. If you skip this and just dump raw extracted tables into Excel side by side, you'll spend every future maintenance session trying to remember which column offset maps to what.
Here's where most intermediate practitioners run into a wall. The Extract data from web page action in PAD is straightforward for static HTML tables. But modern web apps often render tables via JavaScript after the initial page load — meaning the table element exists in the DOM but has zero rows when PAD first tries to extract from it.
The fix is an explicit wait-and-verify loop before extraction. Let's build this into the extraction subflow.
For the SupplierA subflow, the logic looks like this in pseudocode (we'll express this as PAD action blocks):
Subflow: ExtractSupplierA
Input parameters: (none)
Output parameters: SupplierA_Table (DataTable)
STEP 1: Launch browser and navigate
Launch new Microsoft Edge
Initial URL: https://supplierA.internal/inventory
Store browser instance: BrowserInstanceA
STEP 2: Handle authentication (if needed)
Wait for web page to contain element: #username-field
Populate text field on web page: #username-field → %SupplierA_Username%
Populate text field on web page: #password-field → %SupplierA_Password%
Press web page button: #login-btn
Wait for web page navigation: (wait for URL to change)
STEP 3: Wait for dynamic table to load
Set variable: RowCount = 0
Set variable: WaitAttempts = 0
Loop while RowCount = 0 AND WaitAttempts < 20:
Wait: 2 seconds
Get details of element on web page:
Element: css selector → table#inventory-grid tbody tr
Attribute: outerHTML
Store result: TableBodyHTML
If TableBodyHTML contains '<tr' and NOT contains 'Loading...'
Set RowCount = 1 (sentinel to exit loop)
Increase WaitAttempts by 1
If WaitAttempts = 20:
Throw error: "SupplierA table did not load within 40 seconds"
STEP 4: Extract data
Extract data from web page:
Browser instance: BrowserInstanceA
UI element: table#inventory-grid
Store extracted data: SupplierA_RawTable
STEP 5: Close browser
Close web browser: BrowserInstanceA
STEP 6: Return result
Return: SupplierA_RawTable
The wait loop in Step 3 deserves careful attention. We're using Get details of element on web page to retrieve the outerHTML attribute of the table body rows. If the table is still loading, this either throws an element-not-found error (which we catch with an On Block Error handler) or returns a spinner/loading placeholder row. The string check for 'Loading...' catches the second case. The 20-attempt × 2-second ceiling gives the page 40 seconds to render — generous for most apps, but you should tune this based on your specific portal's actual load time.
Warning
Do not use Wait for web page content alone as your dynamic-table guard. That action waits until the page stops making network requests, but many single-page apps maintain persistent WebSocket connections that never fully "idle." You'll either wait forever or timeout. Inspect the actual DOM element for meaningful content instead.
The same pattern applies to SupplierB and SupplierC subflows, with their respective URLs, selectors, and authentication steps. If you need a refresher on building robust selectors that survive minor UI changes, UI Elements and Selectors in Power Automate Desktop: Building Automations That Don't Break covers exactly that.
Before we go further, let's address the credentials that appear in the extraction subflows. In an unattended context, hardcoding usernames and passwords as literal string values is unacceptable — not just for security reasons, but because it makes credential rotation a code-change event rather than an operational one.
The right approach is to store supplier credentials in Azure Key Vault and retrieve them at flow start using the Get secret action from PAD's Azure integration. At the beginning of your main flow, add a block like:
Get secret from Azure Key Vault:
Key Vault name: prod-rpa-vault
Secret name: SupplierA-Credentials
Store result: SupplierA_CredentialJSON
Convert JSON to custom object: SupplierA_CredentialJSON → SupplierA_Cred
Set variable: SupplierA_Username = SupplierA_Cred.username
Set variable: SupplierA_Password = SupplierA_Cred.password [mark as sensitive]
Pass these as input parameters to each extraction subflow. The Handling Credentials Securely in Desktop Flows: Sensitive Variables and Azure Key Vault article covers this pattern in full detail, including how to mark variables as sensitive so they're masked in run logs.
After each extraction subflow returns its raw data table, you need to normalize the columns to your canonical schema. This is where most flows get sloppy — developers leave the raw columns in and handle the mapping in Excel formulas later. That approach is fragile because Excel column formulas break if the web portal ever reorders its table columns.
Do the mapping in PAD, in memory, before writing anything to Excel.
Here's the normalization logic for SupplierA. Their raw columns are [SKU, ProductName, StockQty, UnitCost, LastUpdated] — which happens to match our canonical schema exactly. We just need to add the Source column:
For each CurrentRow in SupplierA_RawTable:
Add row to MasterTable with values:
Column 'Source' = 'SupplierA'
Column 'SKU' = CurrentRow['SKU']
Column 'ProductName' = CurrentRow['ProductName']
Column 'StockQty' = CurrentRow['StockQty']
Column 'UnitCost' = CurrentRow['UnitCost']
Column 'LastUpdated' = CurrentRow['LastUpdated']
SupplierB's raw columns are [ItemCode, Description, Available, Price, Timestamp] — different names, same semantics:
For each CurrentRow in SupplierB_RawTable:
Add row to MasterTable with values:
Column 'Source' = 'SupplierB'
Column 'SKU' = CurrentRow['ItemCode']
Column 'ProductName' = CurrentRow['Description']
Column 'StockQty' = CurrentRow['Available']
Column 'UnitCost' = CurrentRow['Price']
Column 'LastUpdated' = CurrentRow['Timestamp']
In PAD, the Add row to data table action accepts a list of values in column order. Since you're building the MasterTable from scratch with a defined schema (you'll create it at flow start using Create new data table with explicit column headers), this works reliably.
Create the MasterTable at the very beginning of your main flow, before any subflow calls:
Create new data table:
Columns: Source, SKU, ProductName, StockQty, UnitCost, LastUpdated
Store result: MasterTable
This gives you a strongly-typed accumulator that every subflow's normalization loop can populate.
Tip
When using Add row to data table, always pass values as a list in the exact column order you defined when creating the table. PAD does not support adding rows by column name directly — it always uses positional insertion. If your column order ever changes, your normalization loops will silently write data into the wrong columns. Comment your loop clearly with the expected column order.
Now let's wire the main flow together. The top-level structure looks like this:
MAIN FLOW
=== INITIALIZATION ===
Retrieve credentials from Key Vault (SupplierA, B, C)
Create MasterTable with canonical schema
Set variable: FlowStartTime = Current datetime
Set variable: ErrorLog = empty list
=== EXTRACTION PHASE ===
ON BLOCK ERROR (handle SupplierA failure):
Append 'SupplierA extraction failed: %LastError%' to ErrorLog
Set SupplierA_RawTable = empty data table
Run subflow: ExtractSupplierA
Input: SupplierA_Username, SupplierA_Password
Output: SupplierA_RawTable
ON BLOCK ERROR (handle SupplierB failure):
Append 'SupplierB extraction failed: %LastError%' to ErrorLog
Set SupplierB_RawTable = empty data table
Run subflow: ExtractSupplierB
Input: SupplierB_Username, SupplierB_Password
Output: SupplierB_RawTable
ON BLOCK ERROR (handle SupplierC failure):
Append 'SupplierC extraction failed: %LastError%' to ErrorLog
Set SupplierC_RawTable = empty data table
Run subflow: ExtractSupplierC
Input: SupplierC_Username, SupplierC_Password
Output: SupplierC_RawTable
=== MERGE PHASE ===
Run subflow: NormalizeAndMerge
Inputs: SupplierA_RawTable, SupplierB_RawTable, SupplierC_RawTable
Output: MasterTable
=== WRITEBACK PHASE ===
Run subflow: WriteToExcel
Input: MasterTable, FlowStartTime
=== NOTIFICATION PHASE ===
If ErrorLog is not empty:
Send email notification with ErrorLog contents
STOP FLOW
Notice the per-supplier error handling blocks. This is a deliberate design decision: if SupplierA's portal is down, we don't want the entire flow to fail and produce no output. We capture the error, log it, substitute an empty table, and continue. The final report will simply contain no SupplierA rows, and the notification email will explain why. This is the correct behavior for a production unattended flow — partial output with notification beats total failure with silence.
For a deeper treatment of this error handling architecture, see Error Handling in Desktop Flows: On Block Error, Retry Policies, and Recovery Screenshots.
Let's now write out the SupplierA extraction subflow at the full PAD action level. This is the most detailed we'll get — B and C follow the same structure with their own URLs and selectors.
Subflow: ExtractSupplierA
Input parameters:
Username (text)
Password (sensitive text)
Output parameters:
ExtractedTable (data table)
ACTION: Launch new Microsoft Edge
Launch mode: Launch new instance
Initial URL: about:blank
Window state: Normal
Store browser instance as: BrowserA
ACTION: Go to web page
Browser instance: BrowserA
URL: https://supplierA.internal/inventory/dashboard
Wait for page to load: Yes
ACTION: Wait for web page to contain element
Browser instance: BrowserA
UI Element: input[name='username']
Timeout: 30 seconds
ACTION: Populate text field on web page
Browser instance: BrowserA
UI Element: input[name='username']
Text: %Username%
Simulate typing: No (use direct value injection for speed)
ACTION: Populate text field on web page
Browser instance: BrowserA
UI Element: input[name='password']
Text: %Password%
Simulate typing: No
ACTION: Press button on web page
Browser instance: BrowserA
UI Element: button#submit-login
ACTION: Wait for web page to contain element
Browser instance: BrowserA
UI Element: table#inventory-grid
Timeout: 60 seconds
SET VARIABLE: RowCheckAttempts = 0
SET VARIABLE: TableReady = False
LOOP WHILE TableReady = False AND RowCheckAttempts < 25:
ACTION: Wait
Duration: 2 seconds
ON BLOCK ERROR:
SET VARIABLE: RowHTML = ''
(Element may not exist yet — suppress and retry)
ACTION: Get details of element on web page
Browser instance: BrowserA
UI Element: table#inventory-grid tbody tr:first-child
Attribute: innerText
Store result: RowHTML
IF RowHTML <> '' AND RowHTML does not contain 'Loading' AND RowHTML does not contain 'Please wait':
SET VARIABLE: TableReady = True
ACTION: Increase variable RowCheckAttempts by 1
END LOOP
IF TableReady = False:
ACTION: Throw error
Message: 'SupplierA inventory table did not render after 50 seconds'
ACTION: Extract data from web page
Browser instance: BrowserA
UI Element: table#inventory-grid
Store result as: RawTable
ACTION: Close web browser
Browser instance: BrowserA
SET VARIABLE: ExtractedTable = RawTable
A few implementation details worth calling out:
Why about:blank as the initial URL? Launching Edge directly to a login URL can sometimes trigger browser security warnings or cause the page to load before the browser automation hooks are fully established. Starting at about:blank and then navigating is safer for pages that have aggressive anti-bot measures.
Direct value injection vs. simulated typing. The Simulate typing: No setting uses PAD's direct DOM property injection to set field values, bypassing keyboard event simulation. This is significantly faster and more reliable for credential fields — but be aware that some React or Angular applications listen for onChange events that don't fire with direct injection. If a field appears to accept the value but the form doesn't recognize it, switch to Simulate typing: Yes.
The ON BLOCK ERROR around the row check. The first time the loop runs, the table may not exist yet, and Get details of element will throw. Wrapping it in an error block that simply sets RowHTML to an empty string lets the loop continue gracefully rather than crashing.
The normalization subflow receives three raw data tables and iterates through each one, mapping to the canonical schema:
Subflow: NormalizeAndMerge
Input parameters:
TableA (data table) -- columns: SKU, ProductName, StockQty, UnitCost, LastUpdated
TableB (data table) -- columns: ItemCode, Description, Available, Price, Timestamp
TableC (data table) -- columns: PartNumber, Name, Inventory, Cost, Updated
Output parameters:
MergedTable (data table)
ACTION: Create new data table
Columns: Source | SKU | ProductName | StockQty | UnitCost | LastUpdated
Store result: MergedTable
=== Process TableA ===
FOR EACH Row IN TableA:
ACTION: Add row to data table
Data table: MergedTable
Row values: ['SupplierA', Row['SKU'], Row['ProductName'], Row['StockQty'], Row['UnitCost'], Row['LastUpdated']]
=== Process TableB ===
FOR EACH Row IN TableB:
ACTION: Add row to data table
Data table: MergedTable
Row values: ['SupplierB', Row['ItemCode'], Row['Description'], Row['Available'], Row['Price'], Row['Timestamp']]
=== Process TableC ===
FOR EACH Row IN TableC:
ACTION: Add row to data table
Data table: MergedTable
Row values: ['SupplierC', Row['PartNumber'], Row['Name'], Row['Inventory'], Row['Cost'], Row['Updated']]
RETURN MergedTable
If any input table is empty (because an extraction failed and was substituted with an empty table), the FOR EACH loop simply executes zero times — no special handling needed. The design handles failure automatically.
Key insight
Processing empty data tables in a FOR EACH loop is completely safe in PAD — the loop body executes zero times and execution continues. This is why substituting an empty table on extraction failure is so clean. You never need to add an "if table is not empty" guard around your normalization loops.
The writeback subflow is where many flows degrade into fragile hacks — writing row by row in a loop, using hard-coded cell addresses, or appending without clearing stale data first. We're going to do this correctly.
The strategy:
Subflow: WriteToExcel
Input parameters:
DataTable (data table)
RunTimestamp (datetime)
Output parameters: (none)
SET VARIABLE: ExcelPath = 'C:\Reports\SupplierInventory_Daily.xlsx'
SET VARIABLE: SheetName = 'RawData'
SET VARIABLE: HeaderRow = 1
SET VARIABLE: DataStartRow = 2
ACTION: Launch Excel
Document path: %ExcelPath%
Make Excel visible: No (headless for unattended)
Store Excel instance: ExcelInstance
ACTION: Set active Excel worksheet
Excel instance: ExcelInstance
Activate worksheet by name: %SheetName%
=== Clear existing data below header ===
ACTION: Get first free row on column from Excel worksheet
Excel instance: ExcelInstance
Column: 1
Store result: LastUsedRow
IF LastUsedRow > DataStartRow:
ACTION: Delete rows from Excel worksheet
Excel instance: ExcelInstance
Start row: %DataStartRow%
End row: %LastUsedRow%
=== Write new data ===
ACTION: Write to Excel worksheet
Excel instance: ExcelInstance
Value to write: %DataTable%
Write mode: On specified cell
Start column: 1
Start row: %DataStartRow%
=== Write run metadata ===
ACTION: Write to Excel worksheet
Excel instance: ExcelInstance
Value to write: %RunTimestamp%
Write mode: On specified cell
Column: 8 (column H, reserved for metadata)
Row: 1
ACTION: Save Excel
Excel instance: ExcelInstance
Save mode: Save document
ACTION: Close Excel
Excel instance: ExcelInstance
Before closing: Do not save (already saved above)
The Write to Excel worksheet action with a data table as the value performs a batch write — PAD converts the data table into a grid of cell values starting at your specified row and column, writing all rows and columns in a single operation. This is vastly more efficient than a row-by-row loop and completely eliminates the row-index arithmetic bugs that plague loop-based writers.
The "get first free row then delete down to it" pattern for clearing stale data is safer than using a fixed range like A2:F10000. It adapts to actual data size and won't accidentally delete data from other parts of the sheet.
For more advanced Excel patterns including named ranges and worksheet manipulation, Working with Excel Ranges, Sheets, and Named Cells in Power Automate Desktop: Reading Tables, Writing Data, and Switching Worksheets Without Macros covers these in depth.
The flow architecture above is correct for attended runs. For unattended production deployment, you need to address a few additional concerns.
Browser profile isolation. Unattended flows run under the machine's service account, which may have no browser profile configured. If the machine has never logged into supplier portals under the service account, cached sessions won't exist. Always test your credential entry logic under the actual service account context, not your personal developer account.
Display requirements. Even in unattended mode, PAD's browser automation requires an active Windows desktop session. This means your unattended machine needs either a physical display connected, a Remote Desktop session that hasn't been disconnected (not just logged out), or a virtual display driver. Disconnecting an RDP session after starting an unattended flow kills the desktop session and breaks browser rendering. Use tscon or configure your machine to maintain a virtual display. See Attended vs Unattended RPA: Choosing a Run Mode and Configuring Machines in Power Automate for the full setup walkthrough.
Browser update interference. Chrome and Edge update themselves silently and can break PAD's browser extension or change element behavior between runs. Pin your browser version on the unattended machine and disable auto-update via Group Policy.
File locking. The target Excel file must not be open in any other process when the writeback subflow runs. In an unattended context, this is usually guaranteed — but if your Excel file is stored on a network share and someone has it open in read-only mode, some versions of Excel will still refuse to let PAD save it. Store the file locally on the bot machine and use a cloud flow to upload it to SharePoint after the desktop flow completes. For how that handoff works, see Triggering Desktop Flows from Cloud Flows: Passing Inputs and Returning Outputs.
Warning
Never store your output Excel file in a OneDrive-synced folder on the unattended machine. OneDrive's sync process can hold file locks at exactly the moment your flow tries to save, causing intermittent, unpredictable save failures that are extremely difficult to diagnose. Use a local path and copy/upload as a separate step.
Real-world inventory tables often don't present all rows at once — they paginate, loading 50 or 100 rows per page. Your extraction subflow needs to handle this or you'll silently miss most of the data.
The pagination pattern wraps the entire extract-and-merge block in a loop:
SET VARIABLE: PageNumber = 1
SET VARIABLE: MorePages = True
LOOP WHILE MorePages = True:
=== Wait for table to load (as before) ===
ACTION: Extract data from web page
UI Element: table#inventory-grid
Store result: PageData
=== Normalize and add PageData rows to accumulator ===
FOR EACH Row IN PageData:
Add row to MasterTable...
=== Check for next page ===
ON BLOCK ERROR:
SET MorePages = False (next button doesn't exist = last page)
ACTION: If element exists on web page
UI Element: button.pagination-next:not([disabled])
Action if exists: Click
Action if not exists: Set MorePages = False
IF MorePages = True:
=== Wait for new page content ===
Wait: 1 second
(Then loop back to wait-for-table-load)
Increase PageNumber by 1
END LOOP
The key detail here is checking for the disabled state of the Next button, not just its existence. Many pagination implementations keep the Next button in the DOM on the last page but add a disabled attribute. Using the CSS selector button.pagination-next:not([disabled]) distinguishes between a clickable Next button and a disabled one.
This pagination approach is discussed in depth in Automating Multi-Step Web Scraping and Pagination Handling in Power Automate Desktop — worth reading in full if you're building scrapers that need to traverse dozens of pages reliably.
In a single-threaded PAD flow (which all PAD flows are, by default), you don't have traditional race conditions between parallel threads. But you do have timing issues between browser events and your action sequence.
The most common timing bug in dynamic table scraping looks like this:
The fix has two layers:
Layer 1: Stale element detection. After clicking Next, extract a "sentinel" value from the first row of the table — a unique row identifier or even just the row count — and wait until it changes before extracting the full table. This confirms the DOM has updated.
SET VARIABLE: OldFirstSKU = '' (populated from previous page extraction)
LOOP WHILE CurrentFirstSKU = OldFirstSKU:
Wait: 500 milliseconds
Get details of element: table#inventory-grid tbody tr:first-child td:first-child
Store result: CurrentFirstSKU
END LOOP
Layer 2: Loading indicator detection. Many paginated tables briefly show a spinner overlay while loading. Wait for that overlay to disappear before extracting:
LOOP:
Wait: 500 milliseconds
ON BLOCK ERROR: EXIT LOOP (overlay doesn't exist = page loaded)
Get details of element: div.loading-overlay
IF element.style.display = 'none': EXIT LOOP
END LOOP
Combining both layers gives you near-perfect timing reliability across different network conditions.
Apply what you've learned by building a flow that:
Visits two public data sources in separate browser instances — use any two sites with HTML tables (government statistics portals, sports statistics sites, or financial data sites that render public tables work well for practice).
Extracts at least 20 rows from each site, handling the case where the table loads asynchronously (add an artificial wait-and-check loop even if the table is static — this trains the pattern).
Merges both extracted tables into a single data table with a Source column prepended to whatever columns the sites expose.
Writes the merged table to a new Excel worksheet, clearing any existing content first, with the run timestamp written to cell A1 of a separate Metadata sheet.
Wraps each browser subflow in an error handling block that logs failure to a text file and allows the flow to continue with partial data.
Stretch goal: Add pagination support to at least one source. Navigate through three pages, accumulating rows from all three before the merge phase.
Verification: Run the flow twice in succession and confirm that the Excel output from the second run doesn't contain duplicate rows from the first run (proving your clear-before-write logic works correctly).
This happens when PAD's Extract data from web page misidentifies the table structure. It treats the entire <tbody> as a single cell rather than parsing individual <td> elements. The root cause is usually a non-standard table structure — sometimes the site uses <div> elements styled to look like a table, or the actual <table> element is nested inside another table.
Fix: Inspect the page source (not the rendered DOM, but the actual raw HTML) to confirm you're targeting a genuine <table> element. If the site uses CSS grid or flexbox to fake a table structure, you'll need to use a custom extraction approach — either grab the page's raw HTML and parse it with a Run PowerShell action, or identify individual cell elements and extract them positionally.
This occurs when the Launch new Edge action succeeds but the initial URL triggers a certificate error or security warning that PAD can't handle automatically. In an unattended context, there's no user to click "Advanced → Continue anyway."
Fix: Pre-accept any required certificates in the machine's certificate store before the flow runs, or navigate to about:blank on launch and then use the Navigate web browser action which gives you more control over security warnings.
The Extract data from web page action sometimes extracts text from hidden elements or elements whose content is loaded via CSS pseudo-elements (:before, :after). PAD reads innerText, which excludes pseudo-element content.
Fix: Use Get details of element on web page with the innerHTML attribute and parse the content manually, or use a JavaScript execution approach via Run JavaScript function on web page to extract values directly from the DOM.
If you see this error, something else has the file open. In an unattended flow that ran previously and crashed before closing Excel, the Excel process from the previous run may still be alive.
Fix: Add a Close process action at the beginning of your WriteToExcel subflow that terminates any lingering EXCEL.EXE processes. Then add error handling around the Launch Excel action with a retry delay. For robust process management patterns, see Automating Windows Service and Process Management in Power Automate Desktop: Starting, Stopping, and Monitoring Applications Before and After Desktop Flow Execution.
This is the browser cache / session persistence problem. If you're not using about:blank as your launch target and not fully closing the browser between runs (or between retry attempts), the browser may be serving cached page content rather than fresh data.
Fix: Ensure your subflows fully close the browser instance with Close web browser before returning. If you're running the flow multiple times in a test session, also add a Go to web page action with about:blank before navigating to the target URL to force a fresh load, and consider adding Cache-Control: no-cache headers if the site's authentication flow supports it.
This is inevitable. Supplier portals update their HTML structure, class names, and element hierarchies. The table ID changes, or the pagination button gets wrapped in a new container, and your flow starts throwing element-not-found errors.
Fix: Build your selectors with fallback strategies from the start. Use attribute-based selectors ([data-testid='inventory-table']) when available — these are far more stable than class names or positional selectors. For a full treatment of selector resilience, see 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.
Tip
Build a weekly selector health-check flow that simply navigates to each portal, verifies that the key UI elements exist, and sends an alert if any are missing — without attempting any extraction. Running this check on Monday morning gives you advance warning of portal UI changes before your critical Monday morning report run fails.
You've now built a complete, production-grade multi-tab scraping and writeback flow. Let's consolidate what you've actually put together:
Architecturally, you've adopted a three-phase design — extraction, merge, writeback — where each phase is independently testable and each extraction subflow is fully encapsulated with its own browser instance, authentication, dynamic load detection, and error handling. This design means adding a fourth supplier requires writing one new extraction subflow and three additional lines in the normalization loop — not restructuring the entire flow.
Technically, you've implemented explicit dynamic table load detection using sentinel polling rather than the unreliable "wait for page load" approach. You've used CSS selectors with disabled-state awareness for pagination. You've normalized multi-schema sources into a single canonical data table in memory before touching Excel. And you've performed a clean bulk write to Excel using PAD's native data table write mode.
Operationally, you've addressed credential security via Azure Key Vault, error isolation so partial failures produce partial output rather than zero output, and the unattended-specific infrastructure requirements around display sessions and browser version pinning.
Where to go from here:
The natural next step is orchestration at scale. If you have ten suppliers instead of three, or if each supplier extraction takes five minutes and you need the full run to complete in twenty minutes, you'll need to think about parallelism — which means triggering multiple desktop flows simultaneously from a cloud flow orchestrator. That architecture is covered in Building a Resilient Unattended RPA Orchestration Framework in Power Automate Desktop: Queue-Driven Job Dispatch, Machine Load Balancing, and Automated Recovery for 24/7 Production Bot Fleets.
You should also look at how to distribute the generated Excel file downstream — uploading to SharePoint, sending via email, or triggering downstream Power BI refresh. Those patterns are covered in Automating Report Generation and Distribution in Power Automate Desktop: Exporting Data from Windows Applications, Merging into Excel Templates, and Delivering Files via Email Without Human Intervention.
The skills you've developed here — tab context management, dynamic load detection, schema normalization in memory, and unattended-safe writeback — apply directly to any multi-source browser automation scenario you'll encounter in production. These aren't just techniques for one use case; they're the foundation of serious RPA engineering.