Power Automate Desktop Patterns
A working reference for Power Automate Desktop (PAD) — the RPA engine for Windows. Variables and the % notation, data types and structures, control flow, error handling, UI/browser/Excel automation, cloud orchestration, plus the FlowLibs copy-paste designer syntax that pairs with the PAD skills.
Fundamentals
Power Automate Desktop (PAD) is the robotic process automation engine in Power Automate — it drives Windows desktop apps, browsers, files, and legacy systems the way a person would. You build desktop flows in the flow designer by sequencing actions; the console lists your flows and launches runs. Every flow has a Main subflow that runs first, and you call other subflows with Run subflow.
- Actions are the building blocks — each is a configured step (launch Excel, click a button, set a variable). Rename them; the name becomes how you reference outputs.
- Subflows group actions for reuse and readability.
Mainalways exists and always runs first; add more from the subflow tabs. - Variable scope. Global subflows expose variables across the whole flow; local subflows keep variables private — reference a global from a local subflow with the
global.prefix. - Machines & connections. A desktop flow runs on a registered machine (or machine group); a desktop flow connection lets cloud flows sign in and trigger it. Gateways are retired — use direct connectivity.
- Attended vs unattended. Attended runs share your signed-in session; unattended runs spin up their own locked Windows session (no users signed in) and need the Power Automate Process plan.
Author solution-aware from day one
Build inside a Power Platform solution so the desktop flow, its connection references, and environment variables move cleanly across DEV → TEST → PROD. See Best practices & ALM below, and the Naming & ALM guide.
Variables & the % notation
Every variable reference is wrapped in percentage signs: %VariableName%. Anything between % is *evaluated* — a variable, a property, or a whole expression. To print a literal percent character, escape it by doubling: %%.
- Hard-coded text goes in quotes inside an expression:
%'this is ' + 'text'%→this is text. - Property access uses dot notation inside the percents:
%Files.Count%,%CurrentItem.FullName%,%varDate.Year%. - Blank / null. Any variable can hold a Blank value; test it with the Is blank / Is not blank options on conditional actions.
- Names matter. A variable name can’t be a reserved keyword (see the table at the end) — that includes everyday words like
action,step,error, andnext.
| Expression | Result | Notes |
|---|---|---|
| %5 * 3% | 15 | Arithmetic: + - * / |
| %4 / Var% | 4 ÷ value of Var | Numeric result |
| %'Order ' + 5% | Order 5 | + concatenates; number coerced to text |
| %Index = 1 OR Index = 2% | True / False | Logical OR; also AND, NOT, XOR |
| %NOT(4 <> 4)% | True | Comparisons: = <> < <= > >= |
| %Contains(Name, 'SQL', True)% | True / False | arg3 = ignore case |
| %StartsWith(s, 'AVR', False)% | True / False | Also EndsWith / NotStartsWith |
| %IsEmpty(s)% / %IsNotEmpty(s)% | True / False | Empty-string test |
| %(A + B) * C% | grouped math | Parentheses set precedence |
Comparisons need matching types
PAD only compares values of the same type — comparing a number to text throws. Convert first (Text.ConvertTextToNumber, the Value() expression, or read Excel cells as Typed values) before you compare.
Data types
PAD infers a variable’s type from its content. Simple types hold one value; advanced types are collections; instances are live handles to an app or connection that you pass between actions.
| Type | Holds | How to create |
|---|---|---|
| Text value | Any string | Set variable with text, no notation |
| Numeric value | Numbers (only type usable in math) | Set variable with a number |
| Boolean value | True / False | Set variable to %True% or %False% |
| Datetime | Date and/or time | Get current date and time; convert from text |
| List | Single-dimension array | Create new list; or actions that output lists |
| Datatable | Rows × columns (2-D) | Create data table; Read from Excel; SQL; Extract web data |
| Datarow | One row of a datatable | The current item when looping a datatable |
| Custom object | Key/value pairs (JSON-like) | %{ }% empty, or %{ 'k': 'v' }% |
| Instance | Excel / browser / window / SQL handle | Launch Excel, Launch browser, etc. |
Instances are handles, not data
An Excel instance or browser instance identifies *which* app a later action talks to — Launch Excel produces %ExcelInstance%, and every following Excel action takes that instance. Open it, operate, then close it to avoid orphaned processes.
Lists, datatables & custom objects
| Goal | Notation | Example |
|---|---|---|
| List item (0-based) | %List[i]% | %Files[0]% |
| List slice | %List[start:stop]% | %List[2:4]% → 3rd & 4th items |
| List slice to end | %List[start:]% | %List[2:]% |
| List slice from start | %List[:stop]% | %List[:4]% |
| Datatable cell by index | %DT[row][col]% | %ExcelData[0][1]% |
| Datatable cell by column | %DT[row]['Col']% | %ExcelData[0]['Email']% |
| Datatable row slice | %DT[start:stop]% | %ExcelData[2:5]% |
| Datarow item / column | %Row[i]% or %Row['Col']% | %CurrentItem['Status']% |
| Custom object property | %Obj.Prop% | %JsonAsObject.access_token% |
Build a datatable inline
%{ ^['Product', 'Price'], ['Product1', '10 USD'], ['Product2', '20 USD'] }%Indexes are 0-based; loop items are datarows
The first row/column/item is index 0. When you iterate a datatable with For each, the loop variable is a datarow — reach a field with %Row['ColumnName']% (the column names come from the datatable that produced it).
Datatype properties
Many types carry read-only properties you access with %Variable.Property% — no extra action required.
| Type | Property | Returns |
|---|---|---|
| Text | .Length | Character count |
| Text | .ToUpper / .ToLower | Re-cased copy |
| Text | .Trimmed / .isEmpty | Trimmed copy / empty test |
| Datetime | .Year .Month .Day | Date parts |
| Datetime | .DayOfWeek .DayOfYear | Weekday name / day number |
| Datetime | .Hour .Minute .Second | Time parts |
| List | .Count | Number of items |
| Datatable | .RowsCount | Number of rows |
| Datatable | .Columns | List of column names |
| Datatable | .ColumnHeadersRow | Datarow of headers |
| Datatable | .IsEmpty | True if no rows |
| Datarow | .ColumnsCount / .ColumnsNames | Column count / header list |
| Browser instance | .IsAlive | True if the window is still open |
Conditionals
Use If / Else if / Else to branch on any condition, and Switch / Case / Default case to match one value against many. Conditions can be a simple operand-operator-operand comparison or a full %…% logical expression.
| Operator | Meaning | Available in |
|---|---|---|
| = / <> | Equal / not equal | If, Case, Loop condition |
| < <= > >= | Numeric / date compare | If, Case, Loop condition |
| Contains / Does not contain | Substring test | If, Case |
| Starts with / Ends with | Prefix / suffix test | If, Case |
| Is empty / Is not empty | Empty-string test | If, Case |
| Is blank / Is not blank | Null test | If, Case |
One If with a logical expression beats nested Ifs
First operand: %A = 10 AND B = 5%
Operator: Equal to (=)
Second operand: %True%Switch isn’t available in Power Fx flows
In standard desktop flows, Switch / Case / Default case work as expected. If you author a flow in Power Fx mode, Switch/Case/Default case aren’t supported — branch with nested If instead.
Loops
| Action | Repeats | Notes |
|---|---|---|
| Loop | A fixed number of times | Start / increment / end; auto loop-index variable |
| Loop condition | While a condition is true | Two operands + operator; watch for endless loops |
| For each | Once per item | Iterates a list, datatable, or datarow |
| Exit loop | — | Break out early |
| Next loop | — | Skip to the next iteration |
For each over a list (designer copy-paste)
LOOP FOREACH CurrentItem IN $fx'=varFiles'
# ... actions using CurrentItem ...
ENDPrefer For each, and filter before you loop
Looping is the slowest thing PAD does. Filter or sort the source first (Filter data table, Get files with a file filter), and use Exit loop / Next loop to skip work instead of wrapping everything in deeper conditionals.
Flow control & organisation
| Action | What it does | Notes |
|---|---|---|
| Run subflow | Calls another subflow | Supports dynamic name via expression |
| Region | Groups actions visually | Nestable; pure organisation |
| Comment | Annotates the flow | Explain the why |
| Label / Go to | Jump target / jump | Use sparingly — prefer subflows |
| Wait | Pauses N seconds | Last resort vs waiting on a UI element |
| Stop flow | Ends the run | Successfully, or with an error message |
| Exit | Terminates with a code | Optional ErrorMessage to the caller |
| Run desktop flow | Calls another desktop flow | Child runs in the same Windows session |
Subflows vs child desktop flows
Run subflow calls a subflow inside the *same* flow — free and instant. Run desktop flow calls a *separate* desktop flow (shareable across flows) and can pass input/output variables, but adds overhead. Reach for subflows for internal structure; child desktop flows for genuinely shared automation.
Error handling
By default a desktop flow stops on the first error. Assume every UI click, file op, and web call can fail, and decide per action — or per block — what should happen instead.
- Per-action On error. Open an action’s On error tab to set a Retry (N times, every M seconds), then Continue flow run (*Go to next action*, *Repeat action*, or *Go to label*), plus rules to Set variable or Run subflow. Use Advanced to handle different errors differently.
- On block error. Wrap a group of actions in a Block to apply one error policy to all of them — including a Retry policy (None / Fixed / Exponential) and Handle flow terminating errors to also catch logic errors like out-of-bounds indexing or divide-by-zero.
- Get last error. Retrieves the most recent error into an error variable; enable Clear error so you don’t re-read it later.
- Throw custom error. Raises your own named error — place it inside an On block error and add a matching + Custom error entry to handle it.
- Self-healing (preview). For single-element UI/browser actions that hit *Element not found*, an AI fallback can re-find the element at runtime, in the order: Retry → Self-healing → Set variable / Run subflow → Continue / Throw.
FlowLibs Try/Catch pattern (copy-paste)
SomeAction Parameter: $fx'value' Output=> Result
ON ERROR REPEAT 1 TIMES WAIT 2
ON ERROR
CALL Subflow_ErrorLogging
THROW ERROR
ENDBlock-level handler
BLOCK 'Create HTML Table'
ON BLOCK ERROR REPEAT 1 TIMES WAIT 2
ON BLOCK ERROR
CALL Subflow_ErrorLogging
THROW ERROR
END
# ... all actions inside the block ...
END| Access | Returns |
|---|---|
| LastError.Message | Human-readable error message |
| LastError.Location | Where the failure occurred |
| LastError.ErrorDetails | Full detail string |
| LastError['AdditionalInfo'] | Extra info (e.g. cloud-connector body) |
| LastError['Body']['error']['message'] | Nested API error message |
Throw error so failures stay visible
Swallowing an error keeps the run green while the work silently failed. Log it (Dataverse / file / Teams) and Throw error to mark the run failed — unless you have a deliberate dead-letter path that records the bad item and continues.
UI automation & selectors
UI automation drives Windows desktop apps. Each target is a UI element described by one or more selectors — a hierarchy that uses the > notation, where each level is contained in the one to its left. Window-level selectors start from the :desktop root.
:desktop > window[Name="%WindowName%"][Process="Notepad"] > menu "Application" > menu item "File"| Technology | Use for | Notes |
|---|---|---|
| UIA (default) | Modern apps — WPF, WinForms, UWP | Most robust; prefer this |
| MSAA | Legacy Win32 / VB6 apps | When UIA exposes nothing usable |
| UIA3 Raw | Electron / custom-rendered controls | Exposes the full, unfiltered tree |
| Text-based selector | Elements with stable visible text | Capture → “Capture based on text” |
| Image fallback | Apps with no usable accessibility tree | Matches a captured image region |
- Add multiple selectors to one element — PAD tries them in order, so a fallback survives minor UI changes.
- Make selectors static. Replace Equals to with Starts with / Ends with / Contains or a regex to strip dynamic bits (e.g. a trailing
(2)or a counter in a window title). - Test selector and Repair selector from the selector builder before blaming the flow — and recapture on the actual unattended machine, where resolution and DPI differ.
- Run as administrator when automating an elevated app — PAD must run at the same or higher privilege than its target.
Selectors are the #1 cause of flaky RPA
Most unattended failures are *Element not found*: the app updated, the window resized, or the unattended session’s resolution differs from authoring. Build resilient selectors, add fallbacks + a retry policy, and set a fixed screen resolution for unattended runs.
Browser & web automation
Browser automation targets web pages. Launch a browser instance (Chrome, Edge, Firefox, or the automation browser), then act on web UI elements captured with HTML attribute selectors. Web actions need a *browser* instance — desktop UI actions won’t see web elements and vice-versa.
| Action | What it does |
|---|---|
| Launch new browser | Opens Chrome/Edge/Firefox → browser instance |
| Go to web page / Click link | Navigate or follow a link |
| Populate text field on web page | Type into an input |
| Press button on web page | Click a button/element |
| Set drop-down value on web page | Select an option |
| Extract data from web page | Scrape value / table / list (with paging) |
| Get details of web page / element | Read title, URL, attribute, text |
| Run JavaScript function on web page | Execute JS for cases actions can’t reach |
| Handle web dialog | Accept/dismiss JS prompts & auth dialogs |
Prefer a connector or API when one exists
Screen-scraping breaks when layouts change. If the site has an API or a Power Automate connector, call that instead — and reserve Extract data from web page for systems with no programmatic interface.
Excel automation
Excel actions work against an Excel instance from Launch Excel (or Attach to running Excel). Read a range into a datatable, manipulate it in memory, and write it back — far faster than clicking cells through UI automation.
| Action | What it does | Output |
|---|---|---|
| Launch Excel | Open blank or a document | %ExcelInstance% |
| Read from Excel worksheet | Single cell / range / selection / all | %ExcelData% (datatable) |
| Write to Excel worksheet | Write value, list, or datatable | — |
| Get first free row/column | Find the next empty row/column | Index for appends |
| Set active worksheet | Switch sheet by name or index | — |
| Run Excel macro | Run a VBA macro (name;args) | — |
| Close Excel | Save options + release the instance | — |
Excel.ReadFromExcel.ReadAllCells Instance: $fx'=ExcelInstance' GetCellContentsMode: Excel.GetCellContentsMode.TypedValues FirstLineIsHeader: True RangeValue=> ExcelDataOneDrive / SharePoint-synced files break the COM bridge
PAD drives Excel through COM, which isn’t reliable on OneDrive/SharePoint-synced paths — you’ll hit *file not found*. Work on a local copy and sync it back, or open via Run application + Attach to running Excel. Reading Typed values (not text) avoids ### and keeps numbers/dates typed.
Files, folders & text
| Action | What it does |
|---|---|
| Get files in folder | List files, with filter + sort → list of files |
| Get special folder | Resolve Documents/Desktop/etc. paths |
| If file exists / If folder exists | Branch on presence |
| Read text from file | Whole text or list of lines |
| Write text to file | Create/append text |
| Read from / Write to CSV file | CSV ⇄ datatable (encoding, delimiter, headers) |
| Move / Copy / Delete file | File management |
| Filter data table | Multi-condition AND/OR filtering → new datatable |
| Retrieve data table column into list | Pull one column out as a list |
| Text.Replace | Find/replace, literal or regex |
| Crop text / Split text | Substring between flags / split on delimiter |
| Convert date time to text | Format a datetime with a custom pattern |
There is no Text.Length() function
PAD has no Text.Length() expression. To get a character count use the Get text length action (Text.GetTextLength) and read its output; to test for non-empty, use IsNotBlank(varName) in the condition. Windows env vars like %Username% aren’t auto-resolved either — get the path from Get special folder and crop it.
Scripting & Power Fx
When no action fits, drop to a script; and on newer flows you can author logic in Power Fx instead of the classic %…% expressions.
| Mechanism | Return values | Notes |
|---|---|---|
| Run PowerShell script | Write-Output | Use $ for vars; %padVar% to inject |
| Run Python script | Inject PAD vars with %notation% | |
| Run VBScript / JavaScript | WScript.Echo | JS uses var; both echo to output |
| Run .NET script | Script Parameters (In/Out) | Typed in/out arguments |
| Power Fx — Index(list, n) | List item | Power Fx flows use functions, not [i] |
| Power Fx — Index(Index(dt,r),c) | Datatable cell | Row r, column c |
| Power Fx — ${ expr } | Interpolation in text/selectors | Escape a literal with $${ |
Keep scripts thin and idempotent
A one-line PowerShell ([guid]::NewGuid().ToString()) is fine; a 200-line script hidden in an action is a maintenance trap. Prefer built-in actions for portability and logging, and pass values in/out explicitly so the step is testable.
Cloud orchestration
Desktop flows usually run as a step inside a cloud flow. Add Run a flow built with Power Automate for desktop, pick the connection and run mode, and pass data through input/output variables.
- Input variables flow cloud → desktop; output variables flow desktop → cloud. Mark sensitive inputs as sensitive so they’re obfuscated in logs.
- Run modes. *Attended* runs in your signed-in session; *unattended* creates a locked RDP session (all users must be signed out) and needs the Process license. *Picture-in-picture* is attended-only.
- Trigger options. Cloud flow step, the console, a desktop shortcut, or a run URL (browser / Run dialog / Task Scheduler).
| Limit | Value |
|---|---|
| Desktop-flow input size | 2 MB (1 MB in China regions) |
| Runs per minute, per connection | Up to 70 |
| Unattended | Process plan; no users signed in on Win10/11 |
| Run header output type | Resolves at runtime (General at authoring) |
Log to Dataverse for estate-wide visibility
Per-run history lives on the machine. For monitoring across many flows, write a run header + step logs to a Dataverse logging table (the FlowLibs pattern below), and stamp a correlation id so a cloud→desktop transaction can be stitched back together.
PAD copy-paste designer syntax
PAD actions are plain text you can copy straight into the designer — paste them and they become real actions. Getting the string enclosure right is everything: one wrong quote style and the paste fails silently. There are exactly three.
| Enclosure | Use for | Example |
|---|---|---|
| $'''value''' | Fixed text, no runtime vars | SET v TO $'''Info''' |
| %var% inside $'''…''' | Inject a var into a literal | $'''C:\\Temp\\%varUser%''' |
| $fx'…${var}…' | String interpolation | $fx'Processing ${CurrentItem.Name}' |
| $fx'= expr' | Expression evaluation | $fx'= CountRows( varFiles )' |
| Bare reference | Direct var / property / index | SET x TO LastError.Message |
| Output => | Assign an action’s result | Files=> varFiles |
- Backslashes double inside `$'''…'''` —
C:\\Users\\.... This rule does not apply inside$fx'…'. - `%var%` for literal strings, `${var}` for fx strings.
%var%substitutes inside triple-quotes;${var}interpolates inside$fx'…'. - Datatable columns: dot notation inside `$fx`, brackets only when bare.
$fx'${Row.Name}'is valid;$fx'${Row[''Name'']}'is not — useRow['Name']as a bare reference or in an IF condition. - `=>` marks every output (no spaces in the parameter name):
Instance=> ExcelInstance,Response=> RawReturn. - `REGION
/ENDREGION` group actions (double asterisks); name variablesvarCamelCase.
The three enclosures side by side
SET varDownloads TO $'''C:\\Users\\%varUser%\\Downloads'''
SET varMessage TO $fx'Found ${varFileCount} files to clean.'
SET varFileCount TO $fx'= CountRows( varFiles )'
SET varErrorMsg TO LastError.MessageThis pairs with the FlowLibs PAD skills
The same syntax backs the pad-script generator and pad-code-review skills — so a snippet here pastes cleanly into the designer, and a review reads it the same way.
Reusable patterns
Composite, ready-to-copy building blocks. Adapt the variable names; keep the structure.
Logging call
**REGION Logging
SET varCurrentMessage TO $fx'Your message here'
CALL Subflow_Logging
**ENDREGIONEarly exit when there is nothing to do
SET varFileCount TO $fx'= CountRows( varFiles )'
IF varFileCount = 0 THEN
**REGION Logging
SET varCurrentMessage TO $fx'No records found to process'
CALL Subflow_Logging
**ENDREGION
EXIT Code: $fx'=0'
ELSE
**REGION Logging
SET varCurrentMessage TO $fx'Found ${varFileCount} records to process.'
CALL Subflow_Logging
**ENDREGION
ENDProcess loop with counter + per-item logging
SET varProcessedCount TO $fx'0'
LOOP FOREACH CurrentItem IN $fx'=varFiles'
Variables.IncreaseVariable Value: $fx'=varProcessedCount' IncrementValue: $fx'1'
**REGION Logging
SET varCurrentMessage TO $fx'Processing item ${CurrentItem.FullName}'
CALL Subflow_Logging
**ENDREGION
# ... processing logic ...
END
**REGION Logging
SET varCurrentMessage TO $fx'Processed ${varProcessedCount} items'
CALL Subflow_Logging
**ENDREGIONAction guarded by retry + error logging
Folder.GetFiles Folder: $fx'${varPickupFolder}' FileFilter: $fx'*.csv' IncludeSubfolders: False FailOnAccessDenied: True Files=> varFiles
ON ERROR REPEAT 1 TIMES WAIT 2
ON ERROR
CALL Subflow_ErrorLogging
THROW ERROR
ENDBest practices & ALM
- Rename actions and use Regions.
Get_open_ordersbeatsGet_files_3. Names become references; regions make a long Main readable. - Extract reuse into subflows. A logging subflow, an error-logging subflow, and a set-variables subflow keep Main short and consistent.
- Robust selectors. Multiple selectors + static attributes + a retry policy; recapture on the unattended machine.
- Error handling everywhere it can fail. File, web, UI, and cloud-connector actions get an
ON ERROR(retry → log → throw) or sit inside anOn block error. - No secrets inline. Use sensitive inputs, environment variables, or Key Vault — never hard-code credentials, and never log them.
- Solution-aware + versioned. Author in a solution, bump the version each release, and promote DEV → TEST → PROD; keep the Default environment locked down.
- Test on the target machine. Resolution, DPI, app versions, and permissions differ unattended — validate there, not just in the designer.
Document the flow
Capture purpose, trigger, owner, machine/connection, input/output variables, and dependencies in the flow description and a solution README — future-you (and the next maintainer) will need it. See Best Practices and Naming & ALM.
Reserved keywords
These words are reserved by the PAD engine and can’t be used for variable names, custom-object properties, or custom action names/properties. They’re case-insensitive (ACTION = action).
| A – E | F – L | M – S | T – Z |
|---|---|---|---|
| action | false | main | then |
| and | for | mod | throw |
| as | foreach | next | times |
| block | from | no | to |
| call | function | not | true |
| case | global | on | wait |
| default | goto | or | while |
| disable | if | output | xor |
| else | import | repeat | yes |
| end | in | set | — |
| error | input | step | — |
| exit | label | switch | — |
The varCamelCase prefix dodges the whole list
Prefixing every variable with var (and instances by type, like ExcelInstance) keeps you clear of reserved words and instantly readable — varStep, varNext, varError are all fine where step, next, error are not.