Skip to main content

Handling Popups and Dialogs

Popups and dialogs are the most common reason workflows fail. A save prompt appears when you did not expect one; a permission dialog captures focus mid-sequence; a warning blocks the next click. This page covers the patterns for handling each kind.

Expected Dialogs: Inline Handling​

When a dialog is a known, predictable part of the workflow, handle it inline with a Session anchor. Mount it in the phase where it appears, interact with it, unmount it when done.

The Standard Save As Dialog​

The Windows common file dialog is the same across most Win32 applications. Declare it as a Session anchor under the main window:

anchors:
main_window:
type: Root
process: myapp
selector: "*"

save_dialog:
type: Session
selector: "[name='Save As']"

Then handle it in a phase:

- name: save_file
mount: [main_window, save_dialog]
unmount: [save_dialog]
steps:
- intent: trigger Save As
action:
type: Click
scope: main_window
selector: ">> [role=menu item][name=Save]"
expect:
type: DialogPresent
scope: main_window

- intent: set the output filename
action:
type: SetValue
scope: save_dialog
selector: ">> [role='combo box'][name='File name:'] >> [role=edit]"
value: "{param.save_as}"
expect:
type: ElementHasText
scope: save_dialog
selector: ">> [role='combo box'][name='File name:'] >> [role=edit]"
pattern:
contains: "{param.save_as}"

- intent: click Save
action:
type: Click
scope: save_dialog
selector: ">> [role=button][name=Save]"
expect:
type: DialogAbsent
scope: main_window

The filename selector >> [role='combo box'][name='File name:'] >> [role=edit] is safe across Win10 and Win11. On Win10 the inner role=edit has no accessible name — only the parent combo box is identifiable. On Win11 the edit gained its own name, but navigating through the named combo box parent still resolves correctly on both versions.

unmount: [save_dialog] releases the Session handle after the phase so it does not accumulate as a stale reference.

Unsaved-Changes Prompts: Win10 vs Win11​

The unsaved-changes prompt is a good example of how the same logical dialog looks completely different on different OS versions.

Win10: Real Dialog​

On Win10, closing a Notepad window with unsaved changes produces a child dialog window — a separate OS-level dialog attached to the Notepad process. DialogPresent detects it, and the button lives inside the dialog subtree:

- name: close_notepad
steps:
- intent: close the window
action:
type: CloseWindow
scope: notepad
expect:
type: DialogPresent
scope: notepad

- intent: click Don't Save
action:
type: Click
scope: notepad
selector: ">> [role=button][name^=Don][name$=Save]"
expect:
type: WindowClosed
anchor: notepad

Win11: Embedded Prompt​

On Win11, Notepad has a tabbed interface. The unsaved-changes prompt is not a child dialog — it is rendered inline within the window itself after the tab's close button is clicked. DialogPresent does not fire. Instead, the Save/Don't Save buttons appear directly in the window's element tree:

- name: close_notepad
mount: [tab_list]
steps:
- intent: hover tab to reveal its close button
action:
type: Hover
scope: tab_list
selector: "> [role='tab item']"
expect:
type: ElementFound
scope: tab_list
selector: "> [role='tab item'] > [role=button][name^=Close]"

- intent: click the tab close button
action:
type: Click
scope: tab_list
selector: "> [role='tab item'] > [role=button][name^=Close]"
expect:
type: ElementFound
scope: notepad
# The Save/Don't Save buttons appear under [role=window][title=Notepad]
selector: ">> [role=button][name=Save]"

- intent: click Don't Save
action:
type: Click
scope: notepad
selector: ">> [role=button][name^=Don][name$=Save]"
expect:
type: WindowClosed
anchor: notepad

The [name^=Don][name$=Save] selector handles both Don't Save (ASCII apostrophe) and Don't Save (Unicode right quote) without quoting the character.

Unexpected Dialogs: Recovery Handlers​

When a dialog can appear at any point — an error popup, a license warning, a background process notification — handling it inline would require adding a precondition to every step. Recovery handlers are the right tool.

Dismissing a Known Dialog Type​

Use DialogPresent as the trigger to detect any child dialog on the main window, then click the expected dismissal button:

recovery_handlers:
dismiss_dont_save:
trigger:
type: DialogPresent
scope: main_window
actions:
- type: Click
scope: main_window
selector: ">> [role=button][name^=Don][name$=Save]"
resume: retry_step

Generic Catch-All: ForegroundIsDialog​

When you do not know what dialog might appear — or the dialog is application-specific and varies by version — use ForegroundIsDialog. It fires whenever the OS foreground window is a dialog, regardless of which anchor it belongs to. Combined with ClickForegroundButton, it dismisses whatever dialog stole focus:

recovery_handlers:
dismiss_ok:
trigger:
type: ForegroundIsDialog
scope: main_window
actions:
- type: ClickForegroundButton
name: OK
resume: retry_step

ClickForegroundButton clicks the button named OK in the current foreground window — it does not need a scope or selector, so it works even when the dialog is outside the anchor hierarchy.

Enable this handler on phases where unexpected dialogs are likely:

phases:
- name: long_running_operation
mount: [main_window]
recovery:
handlers: [dismiss_ok]
limit: 5
steps:
- ...

Layering Multiple Handlers​

Handlers are checked in order. Put the most specific handler first:

recovery_handlers:
dismiss_errors_dialog:
trigger:
type: ElementFound
scope: main_window
selector: "> [role=dialog][name='Errors or Warnings']"
actions:
- type: Click
scope: main_window
selector: "> [role=dialog][name='Errors or Warnings'] >> [role=button][name=OK]"
resume: retry_step

dismiss_ok:
trigger:
type: ForegroundIsDialog
scope: main_window
actions:
- type: ClickForegroundButton
name: OK
resume: retry_step

The specific Errors or Warnings handler fires first when that particular dialog is present. The generic dismiss_ok catches anything else. If neither handler's trigger matches, the step fails normally.

Dialogs That Should Abort the Workflow​

Not every dialog is safe to dismiss. A fatal error dialog, a data-loss warning, or an authentication prompt that should never appear during automation — these should fail the workflow explicitly rather than being silently dismissed.

Use resume: fail on a handler whose trigger identifies the bad state:

recovery_handlers:
fatal_error:
trigger:
type: ElementFound
scope: main_window
selector: "> [role=dialog][name='Fatal Error']"
actions: []
resume: fail

With an empty actions list and resume: fail, the handler fires, does nothing, and immediately propagates the error — giving you a clear failure message that identifies the dialog rather than a generic timeout.