Learn how to start, stop, and monitor Windows services and scheduled tasks from Power Automate Desktop flows. This hands-on lesson covers native actions, PowerShell integration, polling loops, and production-ready orchestration patterns for background process management.

Here's a scenario that's more common than you might think: your nightly data pipeline depends on a Windows service being in a specific state before your RPA bot starts processing. The ETL service that populates the staging database needs to finish before your desktop flow reads those tables and pushes results into the ERP system. Or maybe you have a legacy batch processing application that runs as a scheduled task, and your automation needs to kick it off, wait for it to complete, and then pull the output files — all without a human watching over it.
This is where Task Scheduler and Windows service management become critical infrastructure for serious RPA work. Power Automate Desktop gives you several routes into this territory: native actions, PowerShell scripting, and the sc.exe / schtasks.exe command-line tools. Knowing which path to take — and how to handle the edge cases — is what separates a fragile automation from one that runs reliably at 2 AM when nobody's watching.
By the end of this lesson, you'll know how to start and stop Windows services from a desktop flow, trigger and monitor scheduled tasks programmatically, build polling loops that wait for background processes to reach the right state, and handle the failures that inevitably happen when services don't cooperate on schedule.
What you'll learn:
You should already be comfortable with the Power Automate Desktop action library and flow structure. You'll need to understand how subflows and reusable logic work in Power Automate Desktop since we'll extract service management logic into its own subflow. You should also have a working understanding of error handling in desktop flows, because service management without error handling is a reliability disaster waiting to happen. Some familiarity with PowerShell is helpful but not required — we'll explain every script we use.
You'll need Power Automate Desktop installed on a Windows machine where you have administrator-level access (or at minimum, the permissions to start/stop specific services and run scheduled tasks).
Before writing a single action, it's worth understanding the three tool tiers you have available, because each has different tradeoffs.
Native PAD actions are the "Services" actions in the System category. They're clean, they integrate with PAD's built-in error handling, and they don't require you to write a single line of script. However, they're limited — you can start, stop, pause, and resume services, but you can't interact with Task Scheduler at all through native actions.
PowerShell is the Swiss Army knife here. The Get-Service, Start-Service, Stop-Service, and Get-ScheduledTask cmdlets give you complete control with rich output you can parse. PAD's "Run PowerShell script" action captures both the standard output and any errors, making it easy to feed results back into your flow logic. If you're already comfortable with scripting inside desktop flows, PowerShell is usually your best tool for Task Scheduler work.
Command-line tools (sc.exe and schtasks.exe) are the oldest approach, available on every version of Windows. They're useful when you need to interact with remote machines or when group policy restricts PowerShell execution. Their output is plain text, which means more parsing work in PAD.
For most scenarios, the pragmatic answer is: use native actions for simple service start/stop, use PowerShell for anything involving Task Scheduler or complex state checks, and fall back to command-line tools when scripting is locked down.
Open a new desktop flow and navigate to the action library. Under "System," you'll find the Services group. The core actions are straightforward: "Start service," "Stop service," "Pause service," and "Resume service." Each takes just a service name as input — and this is where the first gotcha appears.
The service name you need is the internal service name, not the display name. "Windows Update" won't work; wuauserv will. To find the correct name, open Services.msc, double-click any service, and look at the "Service name" field at the top of the Properties dialog (not the "Display name" field). You can also run Get-Service | Select Name, DisplayName in PowerShell to see both columns side by side.
For our pipeline scenario, let's say we need to work with a SQL Server Agent service before our ETL flow runs. The flow structure would look like this:
# Action: Start service
Service name: SQLSERVERAGENT
PAD will attempt to start the service and will throw an error if it fails. This is actually useful — it means you can wrap this in an "On block error" handler and respond intelligently.
Blindly issuing a "Start service" command when the service is already running causes an error that terminates your flow. The right approach is always to check state first. PAD's native actions don't include a "Get service status" action, so for state queries you need to drop into PowerShell.
Add a "Run PowerShell script" action with this script:
$service = Get-Service -Name "SQLSERVERAGENT" -ErrorAction SilentlyContinue
if ($service -eq $null) {
Write-Output "NOT_FOUND"
} else {
Write-Output $service.Status
}
Set the output variable to ServiceStatus. The script outputs one of: Running, Stopped, Paused, StartPending, StopPending, or NOT_FOUND. Back in your flow, add an "If" condition:
If ServiceStatus = "Stopped"
# Action: Start service
Service name: SQLSERVERAGENT
End
This pattern — query, then conditionally act — is far more resilient than blindly issuing commands and hoping for the best.
Tip
When checking service names that might vary between environments (dev vs. prod), store the service name as a flow input variable or retrieve it from a configuration file. This makes your flow portable without code changes. The approach for handling credentials and configuration securely in desktop flows applies equally well to environment-specific service names.
Since you'll perform service state checks repeatedly across different flows, this logic belongs in a dedicated subflow. Create a subflow called CheckAndStartService that accepts a service name as an input variable and returns the resulting service status.
Subflow: CheckAndStartService
Input variable: ServiceName (Text)
Output variable: FinalStatus (Text)
# Step 1: Query current state
Run PowerShell script:
Script: |
$svc = Get-Service -Name "%ServiceName%" -ErrorAction SilentlyContinue
if ($svc -eq $null) { Write-Output "NOT_FOUND" }
else { Write-Output $svc.Status }
Output: CurrentStatus
# Step 2: Conditionally start
If CurrentStatus = "Stopped"
On block error:
Set variable FinalStatus to "START_FAILED"
Go to end of block
Start service: %ServiceName%
Set variable FinalStatus to "Started"
End on block error
Else if CurrentStatus = "Running"
Set variable FinalStatus to "AlreadyRunning"
Else
Set variable FinalStatus to %CurrentStatus%
End If
Now your main flow can call this subflow for any service, check FinalStatus, and branch accordingly. You can also log this result to a text file or SharePoint list for audit purposes — something worth building into any unattended automation that runs overnight.
Task Scheduler has no native PAD actions, so PowerShell is the right tool. The ScheduledTasks module (built into Windows 8/Server 2012 and later) provides clean cmdlets for everything you need.
The most common operation is forcing a scheduled task to run immediately, regardless of its configured schedule. This is how your desktop flow becomes the conductor: it manages the prerequisites, fires the task, waits for completion, and then continues.
Add a "Run PowerShell script" action:
try {
Start-ScheduledTask -TaskName "NightlyETLPipeline" -TaskPath "\"
Write-Output "TRIGGERED"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
}
The -TaskPath parameter specifies the folder in Task Scheduler where the task lives. The root folder is "\". If your task is in a subfolder called "DataOps," the path would be "\DataOps\".
Warning
Start-ScheduledTask returns immediately — it does not wait for the task to finish. If you trigger a task and immediately try to consume its output, you'll be working with stale data. You need a polling loop to wait for completion, which we'll build in the next section.
After triggering a task, you need to poll its state until it reaches Ready (completed) or Running changes to something else. The key cmdlet is Get-ScheduledTask:
$task = Get-ScheduledTask -TaskName "NightlyETLPipeline" -TaskPath "\" -ErrorAction SilentlyContinue
if ($task -eq $null) {
Write-Output "NOT_FOUND"
} else {
Write-Output $task.State
}
Task states you'll encounter:
Ready — Idle, not currently running (also the state after successful completion)Running — Currently executingDisabled — Won't run until re-enabledQueued — Waiting to runThere's a subtlety here: a task that's Ready could mean it hasn't started yet, or it finished successfully, or it failed. To get the actual last run result, you need the task's LastTaskResult property from Get-ScheduledTaskInfo:
$info = Get-ScheduledTaskInfo -TaskName "NightlyETLPipeline" -TaskPath "\"
Write-Output $info.LastTaskResult
A LastTaskResult of 0 means success. Any non-zero value is an error code. The most common ones you'll see: 267009 means the task is currently running, 267011 means the task has not yet run.
Now we can combine these pieces into a proper wait-for-completion loop. This is one of the most important patterns in process orchestration, and getting it right matters for deploying unattended desktop flows at enterprise scale.
Here's the complete flow structure for triggering a task and waiting for it:
# Variables
Set variable TaskName to "NightlyETLPipeline"
Set variable TaskPath to "\"
Set variable MaxWaitMinutes to 30
Set variable PollIntervalSeconds to 15
Set variable ElapsedSeconds to 0
Set variable TaskComplete to false
Set variable TaskSucceeded to false
# Step 1: Trigger the task
Run PowerShell script:
Script: |
try {
Start-ScheduledTask -TaskName "%TaskName%" -TaskPath "%TaskPath%"
Start-Sleep -Seconds 2 # Brief pause to let state update
Write-Output "TRIGGERED"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
}
Output: TriggerResult
If TriggerResult <> "TRIGGERED"
# Log the error and exit
Stop flow
End If
# Step 2: Wait for task to finish
Loop while TaskComplete = false AND ElapsedSeconds < (MaxWaitMinutes * 60)
Wait 15 seconds
Run PowerShell script:
Script: |
$task = Get-ScheduledTask -TaskName "%TaskName%" -TaskPath "%TaskPath%"
$info = Get-ScheduledTaskInfo -TaskName "%TaskName%" -TaskPath "%TaskPath%"
$state = $task.State
$lastResult = $info.LastTaskResult
Write-Output "$state|$lastResult"
Output: TaskCheckResult
# Parse the pipe-delimited output
Split text TaskCheckResult on "|" -> TaskCheckParts
Set variable TaskState to TaskCheckParts[0]
Set variable LastResult to TaskCheckParts[1]
If TaskState = "Ready" AND LastResult <> "267009"
Set variable TaskComplete to true
If LastResult = "0"
Set variable TaskSucceeded to true
End If
End If
Set variable ElapsedSeconds to ElapsedSeconds + 15
End Loop
# Step 3: Check outcome
If TaskComplete = false
# Timeout — task is still running after MaxWaitMinutes
Display message: "Task timed out after %MaxWaitMinutes% minutes"
Else if TaskSucceeded = false
Display message: "Task completed with error code: %LastResult%"
Else
# Task succeeded — continue with downstream processing
End If
Key insight
The Start-Sleep -Seconds 2 inside the trigger script is a small but important detail. Task Scheduler takes a moment to update the task state after you call Start-ScheduledTask. Without the brief pause, your first poll might still see Ready and incorrectly conclude the task already finished.
Sometimes your automation needs to temporarily disable a scheduled task to prevent it from firing at its regular time while your flow is running. This is common when you're running an ad-hoc data load at noon that conflicts with the task's configured 11:55 PM schedule, and you want to prevent a double-run.
# Disable
Disable-ScheduledTask -TaskName "NightlyETLPipeline" -TaskPath "\"
# Re-enable
Enable-ScheduledTask -TaskName "NightlyETLPipeline" -TaskPath "\"
Build this into a try/finally pattern in your flow using PAD's "On block error" to ensure you always re-enable the task even if your flow fails mid-execution:
# Disable the task
Run PowerShell: Disable-ScheduledTask ...
On block error (for the entire processing block):
# Re-enable the task no matter what happened
Run PowerShell: Enable-ScheduledTask ...
Re-raise error
End block
# ... your main processing logic ...
# Re-enable when done
Run PowerShell: Enable-ScheduledTask ...
This guarantees the task won't get stuck disabled if your flow crashes.
For monitoring dashboards or audit flows, you often need to retrieve a list of tasks and their last run status — not just check a single known task. This is useful when building flows that validate the health of multiple scheduled tasks before starting a business process.
$tasks = Get-ScheduledTask -TaskPath "\" | Where-Object { $_.State -ne "Disabled" }
$results = @()
foreach ($task in $tasks) {
$info = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath
$results += "$($task.TaskName)|$($task.State)|$($info.LastRunTime)|$($info.LastTaskResult)"
}
$results -join "`n"
In your PAD flow, capture this output and split it by newline using the "Split text" action, then loop through the results to build a data table. You can then write that table to Excel for a morning health check report — a technique covered in detail in automating Excel with Power Automate Desktop.
There are environments where PowerShell execution is restricted by group policy, or where you need to manage services on a remote machine that your desktop flow's machine can reach over the network. In these cases, sc.exe (the Service Control command) is your fallback.
The syntax for common operations:
# Query service status
sc.exe query SQLSERVERAGENT
# Start a service
sc.exe start SQLSERVERAGENT
# Stop a service
sc.exe stop SQLSERVERAGENT
# Query a service on a remote machine
sc.exe \\REMOTESERVER01 query SQLSERVERAGENT
Use PAD's "Run DOS command" action (found under "System") to execute these. The action captures standard output in an output variable:
# Action: Run DOS command
Command: sc.exe query SQLSERVERAGENT
Output: ScOutput
The output from sc.exe query looks like this (plain text you'll need to parse):
SERVICE_NAME: SQLSERVERAGENT
TYPE : 10 WIN32_OWN_PROCESS
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
To extract the state, use PAD's "Get subtext" or regex capabilities. The most reliable approach is to search for the STATE line and extract the word after the numeric code:
# Action: Get subtext of ScOutput
Start after: "STATE : "
End before: newline
Store result in: StateRaw
StateRaw will contain something like 4 RUNNING. You can then check If StateRaw contains "RUNNING" for a simple state branch.
Note
sc.exe requires administrator privileges to start and stop services. If your flow runs in an unattended session under a service account, make sure that account has the "Service Control Manager" permissions for the specific services you're managing. This is configured in the Windows service's Security settings via the Service Control Manager — not something you can set from PAD directly.
This is where most practitioners hit a wall on their first attempt. Windows service management and Task Scheduler operations require elevated privileges, and Power Automate Desktop flows don't automatically run as administrator.
For unattended flows: The machine credential configured in the PAD machine registration determines the privilege level. If you're running unattended automation and your service account has the necessary rights (either local admin or explicit service management permissions), operations will succeed without UAC prompts. See attended vs. unattended RPA considerations for how this interacts with your run mode.
For attended flows: The flow runs as the currently logged-in user. If that user doesn't have admin rights, you'll get "Access Denied" errors. You have two options:
Use a wrapper PowerShell script that's configured to run with elevated privileges through a scheduled task (with "Run with highest privileges" checked), and trigger that task from your flow. Your flow triggers the privileged task; the task does the actual service management.
Grant targeted permissions using the sc.exe sdset command or Group Policy to allow specific service operations without full admin rights. This is the more secure approach for production.
Warning
Never store administrator credentials as plaintext in your flow to work around privilege issues. Use the service account approach for unattended flows, or the scheduled task wrapper approach for attended flows. Hardcoding credentials is a security violation and will eventually cause incidents.
Let's build something complete and realistic. Your company has a nightly data warehouse load that depends on four things being true before it can safely run:
Runningstaging_ready.flag must exist in C:\ETL\signals\If all four checks pass, the flow triggers the load. If any fail, it logs the failure and sends a notification (via a cloud flow trigger — a pattern covered in triggering desktop flows from cloud flows).
Here's the flow skeleton:
# === Pre-Flight Check Subflow ===
Subflow: RunPreFlightChecks
Output variable: AllChecksPassed (Boolean)
Output variable: FailureReason (Text)
# Check 1: SQL Server Agent
Run PowerShell:
$svc = Get-Service -Name "SQLSERVERAGENT" -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq "Running") { Write-Output "PASS" }
else { Write-Output "FAIL: SQLAgent not running - Status: $($svc.Status)" }
Output: Check1Result
# Check 2: StageDBRefresh task last run
Run PowerShell:
$info = Get-ScheduledTaskInfo -TaskName "StageDBRefresh" -TaskPath "\"
$today = (Get-Date).Date
$lastRun = $info.LastRunTime.Date
$lastResult = $info.LastTaskResult
if ($lastRun -eq $today -and $lastResult -eq 0) { Write-Output "PASS" }
else { Write-Output "FAIL: StageDBRefresh - LastRun: $lastRun, Result: $lastResult" }
Output: Check2Result
# Check 3: Flag file exists
If (File exists C:\ETL\signals\staging_ready.flag)
Set Check3Result to "PASS"
Else
Set Check3Result to "FAIL: staging_ready.flag not found"
End If
# Check 4: DW load task not already running
Run PowerShell:
$task = Get-ScheduledTask -TaskName "DataWarehouseLoad" -TaskPath "\"
if ($task.State -eq "Running") { Write-Output "FAIL: DW load already running" }
else { Write-Output "PASS" }
Output: Check4Result
# Evaluate all checks
Set variable AllChecksPassed to true
Set variable FailureReason to ""
For each CheckResult in [Check1Result, Check2Result, Check3Result, Check4Result]:
If CheckResult starts with "FAIL"
Set variable AllChecksPassed to false
Set variable FailureReason to FailureReason + CheckResult + " | "
End If
End For
# === Main Flow ===
Call subflow: RunPreFlightChecks
If AllChecksPassed = true
# Trigger the load task
Run PowerShell:
Start-ScheduledTask -TaskName "DataWarehouseLoad" -TaskPath "\"
Write-Output "TRIGGERED"
# ... polling loop as described earlier ...
# Log success to CSV
Write to CSV: C:\ETL\logs\load_history.csv
Columns: Date, TaskName, Status, Duration
Else
# Log failure
Append to file: C:\ETL\logs\preflight_failures.log
Content: %CurrentDateTime% - PRE-FLIGHT FAILED: %FailureReason%
# Signal cloud flow to send alert email
# (via a trigger file or HTTP endpoint)
End If
This represents a genuinely production-ready orchestration pattern. The subflow is testable in isolation, the checks are explicit and logged, and failures produce actionable information rather than cryptic errors. You can evolve this into a full monitoring framework by storing check results in a data table and writing them to a SharePoint list or database.
Build a desktop flow that performs the following sequence:
Scenario: You manage a Windows machine that runs a local instance of a reporting service called ReportServer (SQL Server Reporting Services, if available — or substitute any service on your machine) and a scheduled task that generates a daily export.
Create a subflow called ServiceHealthCheck that:
Spooler — the Print Spooler, which is safe to test with) is RunningHealthy, Stopped, or Unknown as a text output variableIn your main flow:
ServiceHealthCheck and capture the outputStopped, start it using PAD's native "Start service" action wrapped in an error handlerC:\PAD_Logs\service_errors.log (create the folder if it doesn't exist)Add a scheduled task check using PowerShell:
Get-ScheduledTask | Select TaskName, State in PowerShell to find candidates)[timestamp] Task: [name] | State: [state] | LastResult: [code]Add a timeout mechanism: if the service takes more than 60 seconds to reach Running state after you start it, write a timeout error to the log and stop the flow.
This exercise gives you hands-on experience with every core pattern: native actions, PowerShell integration, subflows, error handling, and file logging.
The "Start service" action in PAD requires the internal service name. If you get an error like "The specified service does not exist as an installed service," open services.msc, find your service, and copy the "Service name" from the Properties dialog — not the display name.
Services don't transition instantaneously. A service might be in StartPending for 30 seconds while it initializes. If your flow checks for Running immediately after sending a start command, it might see StartPending and incorrectly treat it as a failure. Always implement a short poll loop after issuing a start command, checking for Running rather than immediately comparing.
As discussed earlier, Ready just means the task isn't currently running. It doesn't distinguish between "hasn't run yet today," "finished successfully," and "finished with an error." Always check LastTaskResult alongside State.
If your flow disables a task and then crashes before re-enabling it, that task is now permanently disabled until someone manually intervenes. Always use error handlers that re-enable tasks in their cleanup block. Make this non-negotiable.
If you see "Access is denied" errors from PowerShell scripts or the "Start service" action, the issue is almost always that the session running the flow doesn't have the right permissions. Verify the service account's permissions before blaming your flow logic. Run whoami in a "Run DOS command" action to confirm which user your flow is executing as.
When a service fails to start at 3 AM, the only thing you have to diagnose the problem is your log file. Log the service name, the attempted action, the result (including error codes), and a timestamp for every operation. A CSV log file that captures each run's pre-flight results is worth its weight in gold when you're troubleshooting an incident.
Tip
Add a "Run PowerShell script" action that captures Get-EventLog -LogName System -Source "Service Control Manager" -Newest 5 whenever a service fails to start. This gives you the actual Windows event log entries explaining why the service didn't start, which is dramatically more useful than a generic "start failed" message in your own log.
If your "Run PowerShell script" action completes without error but the output variable is empty, check these things in order:
Execution policy: Run Get-ExecutionPolicy in PowerShell to check the current policy. If it's Restricted, scripts won't run. You may need to add -ExecutionPolicy Bypass to the invocation, or work with your IT team to adjust the machine policy.
Use Write-Output, not Write-Host: PAD captures stdout from PowerShell. Write-Host goes to the console host, not stdout, and won't be captured. Always use Write-Output or return.
Check the ScriptError variable: PAD's "Run PowerShell script" action produces both an output variable and a script error variable. Check the error variable — it often contains the actual exception message that explains the empty output.
You've now built a complete toolkit for orchestrating Windows background processes from Power Automate Desktop flows. The key patterns to take away:
The logical next step is integrating this orchestration layer with unattended runs at scale. When your flow manages services as part of an enterprise pipeline, you need to think carefully about concurrency — what happens when two instances of your flow try to start the same service simultaneously? That problem lives at the intersection of desktop flow architecture and enterprise-scale unattended deployment strategies.
You should also consider how to surface the health data your flows generate into a monitoring dashboard. The log files and status outputs you're already capturing can feed into Power BI or a SharePoint list via a cloud flow, giving operations teams visibility without needing to log into the automation machine directly. Connecting your desktop flow's outputs to a cloud flow trigger is exactly the kind of integration covered in monitoring and troubleshooting desktop flow runs at scale.
Service and task orchestration might seem like system administration territory, but for serious RPA practitioners, it's the difference between automations that run reliably in production and ones that fail silently because the environment wasn't ready. Master this layer, and your flows become genuinely autonomous.
Power Automate Desktop & RPA
Automating Windows Dialog Boxes and Pop-Up Windows in Power Automate Desktop: Handling Alerts, File Pickers, and Modal Prompts Reliably
Automating Windows Registry and Environment Variable Operations in Power Automate Desktop: Reading, Writing, and Managing System Configuration for Application Automation