Task automation on Windows, with step-level recovery built in

Task Scheduler runs a program and reports an exit code. RPA canvases draw a flowchart. Neither gives you a per-step retry count, a named fallback target, a post-success jump table, or a UI-diff contract on a single UI action. Terminator does, and the struct that encodes them lives in one file you can read in a minute.

M
Matthew Diakonov
9 min read
4.9from design partners running this in prod
One struct, SequenceStep, holds all four recovery primitives
Separate troubleshooting array keeps recovery off the happy path
Workflows are YAML an AI coding assistant writes for you via MCP

The ceiling of traditional task automation on Windows

Every list of task automation on Windows tools that the SERP will recommend (Task Scheduler, Power Automate Desktop, AutoHotkey, RoboTask, UiPath, AutoIt) is a list of ways to trigger a piece of code and a way to log that the code ran. The model is binary: the program exited with status 0 or it did not. What happens when a dialog does not appear, when a button got clicked but the wrong one, when the network was flaky on exactly the third retry, is something you write glue code around, not something the scheduler understands.

This matters because the failure modes of Windows UI are not binary. A click can land on a stale toast, a modal can open in another process, a Save dialog can appear on the next frame instead of this one. The right shape for a step in a Windows automation is not “did the .exe exit clean” but “is the UI now in the state this step was supposed to produce.” That is a per-step contract, and nothing in the traditional task automation stack on Windows lets you express it.

Terminator was built after we got tired of paying that glue cost. The entire recovery vocabulary is in one struct.

0 fieldson SequenceStep
0/11govern control flow or verification
0dedicated to retries and branching
0xmax troubleshooting depth before hard fail

The anchor fact: SequenceStep has 11 fields, 9 are about recovery

This is the struct that every step in an execute_sequence workflow deserializes into. Read it end to end. Count the fields that describe what to click, versus the fields that describe what should happen if the click does not land the way you planned.

utils.rs

Only tool_name and arguments describe what to do. Nine other fields describe the safety net around it. That ratio is the design choice.

Four primitives, one struct

These are the fields that make a Terminator workflow a workflow and not a script. Each one has a single, small responsibility, and they compose.

retries

A u32 on every step. The engine runs the tool up to retries+1 times before treating the step as failed. Default 0. Applies equally to steps in the troubleshooting array.

fallback_id

A string that must match another step's id. When set and the step fails after retries, execution jumps there. The target can live in the main steps array or in the separate troubleshooting array.

jumps

An array of JumpCondition entries. After a successful run, the engine walks them in order and jumps to the first to_id whose condition evaluates to true. Fires only on success.

expected_ui_changes

A description of the UI diff this step should produce. Paired with ui_diff_before_after capture in execution_logger.rs, it gives you a step-level contract for 'did the click actually work?'

if

A boolean expression evaluated against the env before the step runs. If false, the step is skipped. Use this to gate a step on a previous step's result or an input variable.

id

The addressable name of this step. Also stores {id}_result and {id}_status in the env so later steps can reference them in jumps.condition or if.

jumps: post-success branching without a wrapper script

fallback_id fires on failure. jumps fire on success. Together they cover both sides of a step's outcome. A JumpCondition is three fields, and the engine walks them in order after the tool returns ok.

utils.rs

How the engine actually runs one step

The main loop in server_sequence.rs. Every field from the struct shows up here in a specific order: if gate, retry loop, fallback on failure, jumps on success. Nothing is hidden behind magic.

server_sequence.rs
9/11

9 of the 11 fields on SequenceStep govern control flow or verification, not which button to click.

crates/terminator-mcp-agent/src/utils.rs:1453

How a workflow routes one failed step

A typical Save action on a Windows accounting app. Three main steps. If the Save button ever fails past its retries, execution jumps into a troubleshooting step that reopens the dialog, then the main flow resumes exactly where it left off.

execute_sequence control flow

AgentMainRetryFallbackEnvrun step invoke Saveattempt 1ui_diff mismatchattempt 2 (retry)ui_diff mismatchjump to fallback_idwrite recovery contextenv updatedrecovery ok, resume mainstep save_invoice complete

A real workflow you can read in one sitting

Three main steps, two troubleshooting steps, all four recovery primitives used at least once. This is what your AI coding assistant writes when you ask Claude Code or Cursor to automate an invoice sync over MCP.

invoice-sync.yaml

Task Scheduler on the left, Terminator on the right

Same intent, a Save button retry with a recovery path. Task Scheduler can retry the whole program on failure. It has no concept of a named target to jump to, a post-success condition, or a UI change the step must produce.

Task Scheduler XML

scheduled-task.xml

Terminator execute_sequence

save-invoice.yaml

Feature by feature

The competitor column is a composite of Task Scheduler, AutoHotkey, and the common RPA canvases. The ours column points at the exact place in the Terminator source where the primitive lives.

FeatureTask Scheduler / RPA canvasesTerminator
Per-step retry countOnly whole-program retryretries: u32 on every SequenceStep
Named recovery target on failureNot supportedfallback_id jumps to a specific step id
Post-success conditional branchingMust shell out to a wrapper scriptjumps: array of if/to_id/reason entries
Recovery steps kept out of the happy pathMixed into the main scriptSeparate troubleshooting array, never runs unless jumped to
UI change contract per stepNot expressibleexpected_ui_changes string on every step
Resume from a specific step idReruns the whole programstart_from_step loads persisted env, resumes
Written by an AI coding assistantGUI point-and-click or hand-writtenJSON schema is exposed over MCP, Claude or Cursor writes the YAML

What an actual run looks like

A MCP client drives a three-step workflow through Claude Code or Cursor. The first step fails twice, falls back to a reauth step, then the main flow resumes at exactly the place it left off.

execute_sequence

Tools this supplants or fronts

Most of these work well for triggering a program. None expose a per-step fallback target, a post-success jump condition, or a UI diff contract at the step level.

Windows Task SchedulerPower Automate DesktopAutoHotkeyRoboTaskUiPathBlue PrismAutoItWinAutomationAutomation WorkshopTinyTaskMacro RecorderWinAppDriver

Why this matters for AI coding assistants

Every popular AI coding assistant can already write code. What Terminator adds is a runtime that an assistant can target with a workflow that admits failure gracefully. Claude Code, Cursor, and Windsurf all speak MCP. When you install terminator-mcp-agent, the assistant sees execute_sequence in its tool list, with the SequenceStep schema exposed via JsonSchema.

That means when you ask the assistant to “automate the invoice sync and handle login expiry,” it has the exact vocabulary (retries, fallback_id, jumps, troubleshooting) as first-class arguments. It does not invent its own retry loop in a wrapper script. It writes a YAML workflow that Terminator runs the same way twice.

The differentiator versus other task automation on Windows tools is not just per-step recovery. It is per-step recovery that a model writing your automation actually uses, because the schema is in the tool description the model reads.

Verify in the repo

If you want to confirm none of this is marketing copy, four grep lines. Every field I named has a line number, and every line number is reachable from a fresh clone.

zsh

When not to use Terminator for task automation

If all you need is “run C:\scripts\nightly.ps1 at 3am,” Task Scheduler is the right answer. It is already on the box, and the reliability of cron-style triggers is something Terminator does not try to replace. Terminator sits on the other axis: once the script starts running, how does it handle the fact that the UI it is poking at was designed by humans for humans and does not answer a wire protocol.

The healthy pattern is Task Scheduler triggers the workflow, and the workflow itself is a Terminator execute_sequence call. The scheduler decides when. Terminator decides how to recover when the UI does not do what you expected.

Want to see your worst flaky Windows task survive a real retry-and-fallback run?

Book 20 minutes. Bring one of your most fragile scheduled tasks. We will map it to SequenceStep primitives and show you where retries, fallback_id, and expected_ui_changes each catch a specific failure in your stack.

Frequently asked questions

How is task automation on Windows with Terminator different from Windows Task Scheduler?

Windows Task Scheduler runs a program on a trigger and reports an exit code. It has no concept of 'the Save dialog did not appear, so jump to this recovery step.' Terminator's execute_sequence engine is a workflow runner that wraps every UI step with four recovery and branching primitives: retries, fallback_id, jumps, and expected_ui_changes. The fields are defined on the SequenceStep struct in crates/terminator-mcp-agent/src/utils.rs at line 1453, and the retry plus fallback loop lives in crates/terminator-mcp-agent/src/server_sequence.rs at line 1116.

What does fallback_id actually do when a step fails?

When a step fails after exhausting its retries, the engine looks up the step whose id equals the failed step's fallback_id and jumps execution there. The fallback target does not need to live in the main steps array. The ExecuteSequenceArgs struct has a separate troubleshooting array (utils.rs line 1520) whose entries are invisible on the happy path and only run when a fallback_id jumps to them. That lets you keep the linear workflow readable while parking recovery logic in a clearly named sidecar block.

What is the difference between jumps and fallback_id?

fallback_id fires on failure; jumps fire on success. The jumps field is an array of JumpCondition entries, each with an if expression and a to_id target (utils.rs line 1440). After a step returns success, the engine walks the jumps array in order, evaluates the if expression against the accumulated env, and jumps to the first matching target. So a single step can declare 'if the response body says retry=true, go to step X, otherwise fall through to the next step in sequence.' That gives you post-success branching without wrapping every decision in a wrapper script.

Why does expected_ui_changes matter for task automation on Windows?

Most Windows automation failures are silent. The click returned success because some button got clicked, but it was the wrong button, or the dialog did not actually open. Terminator pairs every step with a ui_diff_before_after capture (handled in execution_logger.rs around line 940) and an optional expected_ui_changes string on the step. When set, the engine can fail the step if the real UI diff does not match the plan, which kicks in retries and fallback_id the same way any other failure would. You get 'did the click actually work?' as a first-class step contract.

Can I resume a failed task automation workflow halfway through?

Yes. ExecuteSequenceArgs exposes start_from_step and end_at_step (utils.rs lines 1563 and 1566). start_from_step loads the previously persisted env state and resumes execution at the step whose id matches. end_at_step stops inclusively at a step id. Combined with a workflow_id, that gives you bounded reruns where you skip work that already succeeded on the previous attempt. Task Scheduler cannot do this; it reruns the whole program.

What happens if the fallback step itself fails?

The retry budget and fallback behaviour apply to the troubleshooting step too. Each entry in the troubleshooting array is a full SequenceStep with its own retries, its own jumps, and its own optional fallback_id. The engine protects itself from infinite ping-pong with max_iterations = sequence_items.len() * 10 (server_sequence.rs line 1021), after which the workflow fails hard. In practice that cap means a troubleshooting chain can be up to ten levels deep before the engine gives up.

How do the retries, fallback, jumps, and ui-diff fields interact with each other?

The order in server_sequence.rs around line 1116 is explicit. First the if expression is evaluated against the env; if it is false, the step is skipped. Otherwise the engine enters a retry loop up to retries+1 attempts. Inside each attempt the tool runs and, if ui_diff_before_after is enabled, the diff is captured and compared against expected_ui_changes. A mismatch counts as a failure and consumes a retry. Once the retry budget is spent, if fallback_id is set, execution jumps there; otherwise the step is marked critical and the workflow halts unless continue is true. If the step did succeed, the jumps array is walked and the first matching condition wins.

What is the actual YAML or JSON I write to use these fields?

execute_sequence takes either inline steps or a url to a YAML or JSON workflow file. A step block with all four primitives looks like a YAML entry with tool_name, arguments, id, retries, fallback_id, jumps (a list of if / to_id / reason objects), and expected_ui_changes. The MCP tool description in crates/terminator-mcp-agent/src/prompt.rs describes the shape Claude or Cursor should emit. Because the schema is strongly typed via serde and schemars, your AI coding assistant autocompletes the fields when it writes the workflow instead of guessing.

terminatorDesktop automation SDK
© 2026 terminator. All rights reserved.