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.