Go beyond PAD's built-in actions by embedding PowerShell, Python, VBScript, and batch scripts directly in your desktop flows. Learn how variable injection works, how to return structured data, and how to build secure, maintainable scripting blocks for production RPA.

You've built desktop flows that click buttons, fill forms, and navigate through legacy UIs. They work — but eventually you hit a wall. You need to query a registry key, call a COM object, reshape a data structure, or run a computation that no built-in action handles cleanly. You could chain together twenty Power Automate Desktop actions to approximate what three lines of PowerShell would do in half a second. Or you could use the scripting actions.
Power Automate Desktop ships with native action blocks for running PowerShell, Python, VBScript, and DOS batch scripts directly inside a flow. These aren't afterthoughts. They're first-class extensibility points designed for exactly this scenario: when the built-in action library ends and the real work begins. They let you mix declarative automation — drag-and-drop UI interactions, Excel operations, file system actions — with arbitrary code running in the same process context, passing variables bidirectionally. The output of a PowerShell script becomes a PAD variable. A PAD variable can be injected directly into a Python script. This bidirectional handshake is what makes scripting actions genuinely powerful rather than just a workaround.
By the end of this article, you'll understand how each scripting runtime integrates with the PAD variable system, how to pass inputs and capture structured outputs, where each language excels and where it creates problems, and how to design your scripting blocks so they don't become unmaintainable black boxes. You'll also learn the security implications that matter enormously in unattended scenarios.
What you'll learn:
This lesson assumes you're comfortable with the PAD designer and have built flows beyond the introductory level. You should understand variables, lists, and data tables in Power Automate Desktop well enough that terms like "list variable" and "data table" don't need explanation. Familiarity with at least one of PowerShell, Python, or VBScript is expected — we'll show PAD-specific integration patterns, not teach the languages from scratch. Understanding error handling in desktop flows will help you implement the defensive patterns described later.
Before writing a single line of code, you need to understand what actually happens when PAD runs a scripting action. The mental model matters because it affects how you pass data, how you debug failures, and why certain things that "should work" don't.
When PAD hits a scripting action, it does the following:
The child process runs in the security context of the PAD agent — the same user or service account executing the desktop flow. On an attended machine, that's your interactive session. In unattended mode running as a service, that's the service account, which almost certainly has no interactive desktop and might have restricted network access. This distinction is critical and we'll return to it in the security section.
The key implication of step 1 — string substitution for input variables — is that PAD does not have a real inter-process communication channel. It's not passing serialized objects or using shared memory. It's doing text interpolation. This is powerful but also a source of bugs: if your PAD variable contains quotes, newlines, or curly braces, the interpolated script may be syntactically invalid. We'll cover quoting and escaping strategies in each language section.
Warning
PAD's scripting actions do not maintain state between executions. Each script block spawns a new process. If you set a variable in one PowerShell block and expect it to persist in a later PowerShell block, you'll be disappointed. Design your scripts to be self-contained units, not stateful sessions.
PowerShell is the first tool you should reach for in any Windows-centric automation. It has native access to .NET, WMI, the registry, Active Directory, COM objects, and thousands of built-in cmdlets. Its output formatting system — particularly when combined with ConvertTo-Json — makes it the most ergonomic option for returning structured data back to PAD.
In the PAD designer, find the action under Scripting > Run PowerShell Script. The action has these key fields:
The code editor is a plain text area — there's no syntax highlighting. Write your scripts in VS Code or PowerShell ISE first, test them standalone, then paste them in.
PAD injects variables using its %VariableName% syntax directly into the script text before execution. Here's a simple example: if you have a PAD variable %FilePath% containing C:\Reports\Q4.xlsx, this script body:
$path = "%FilePath%"
$fileInfo = Get-Item $path
Write-Output $fileInfo.Length
...becomes, at runtime:
$path = "C:\Reports\Q4.xlsx"
$fileInfo = Get-Item $path
Write-Output $fileInfo.Length
That works cleanly. But consider what happens if %FilePath% contains a path with a double quote in it, or if you're injecting a PAD variable that holds user-submitted text. The naive interpolation breaks immediately. The defensive pattern is to always assign injected values to variables at the top of the script and sanitize them there:
# Inject at the top as a raw assignment
$RawInput = '%UserInput%'
# Sanitize before use
$SafeInput = $RawInput -replace "'", "''"
$SafeInput = $SafeInput.Trim()
For numeric PAD variables, the injection works without quotes:
$threshold = %ThresholdValue%
if ($threshold -gt 100) {
Write-Output "exceeded"
} else {
Write-Output "within range"
}
Tip
Always test your scripts with adversarial input values — strings containing quotes, backslashes, percent signs, and newlines. The PAD variable substitution is naive text replacement, so production data will find edge cases that your clean test data never will.
Everything you Write-Output (or that lands on the pipeline without being captured) goes to stdout and becomes the content of your output variable. For simple scalar values, this is trivial. For structured data, use JSON:
$serverName = "%TargetServer%"
# Gather system information
$diskInfo = Get-PSDrive C | Select-Object Used, Free
$osVersion = (Get-WmiObject Win32_OperatingSystem).Caption
$result = @{
DiskUsedGB = [math]::Round($diskInfo.Used / 1GB, 2)
DiskFreeGB = [math]::Round($diskInfo.Free / 1GB, 2)
OSVersion = $osVersion
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
}
Write-Output ($result | ConvertTo-Json -Compress)
Back in PAD, your output variable (say %PSOutput%) contains the JSON string. You then use a Parse JSON action or string manipulation to extract the values you need. For a flat JSON object like this one, the fastest approach is to use the Convert JSON to Custom Object action and then access properties with dot notation.
For returning lists, output a JSON array:
$reportDir = "%ReportDirectory%"
$files = Get-ChildItem $reportDir -Filter "*.xlsx" |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) } |
Select-Object Name, @{n='SizeKB'; e={[math]::Round($_.Length / 1KB, 1)}}, LastWriteTime |
ConvertTo-Json -Compress
Write-Output $files
Then in PAD, parse the JSON array and loop through it using a For each loop over the resulting list.
By default, PowerShell's error behavior ($ErrorActionPreference) is Continue, which means non-terminating errors don't stop execution and might not appear in the stderr capture. Set this explicitly at the top of every production script:
$ErrorActionPreference = 'Stop'
try {
$serverName = "%TargetServer%"
# The real work
$result = Invoke-Command -ComputerName $serverName -ScriptBlock {
Get-Service -Name "SQLServer" | Select-Object Status
}
Write-Output ($result | ConvertTo-Json -Compress)
}
catch {
# Write a structured error to stdout so PAD can inspect it
$errorInfo = @{
Success = $false
Error = $_.Exception.Message
Line = $_.InvocationInfo.ScriptLineNumber
} | ConvertTo-Json -Compress
Write-Output $errorInfo
exit 1
}
In PAD, check the output variable for a Success: false JSON flag, or use the On block error mechanism to catch the non-zero exit code. The PAD scripting action will mark itself as failed if the script exits with a non-zero exit code — but only if you explicitly call exit 1. Uncaught exceptions that PowerShell handles gracefully with its default behavior may not surface as PAD failures at all.
The PowerShell script action runs with whatever execution policy is configured for the machine. On a developer's workstation, this is often RemoteSigned or Bypass. On a hardened enterprise machine or a PAD service account, it might be Restricted, which will prevent your scripts from running entirely.
The PAD action effectively runs something equivalent to:
powershell.exe -ExecutionPolicy Bypass -File "C:\Users\...\Temp\pad_script_xxx.ps1"
PAD passes -ExecutionPolicy Bypass to the powershell.exe invocation, so the machine's configured policy is bypassed for PAD-spawned scripts. This is useful to know: you don't need to change the machine's execution policy, and you also can't rely on the machine's policy to restrict what PAD scripts can do.
Warning
Because PAD bypasses execution policy, scripting actions are a potential security gap in environments where execution policy is used as a control. Work with your security team to understand this before deploying unattended bots with PowerShell actions. See the security section later in this article.
Python is the right choice when you need data science libraries, complex text processing, API clients, or when your team's expertise is in Python rather than PowerShell. PAD's Python action works similarly to PowerShell but has some meaningful differences in setup and output handling.
Unlike PowerShell (which ships with Windows), Python must be installed on the machine separately. PAD does not bundle a Python runtime. You need:
This is the first major architectural decision with Python scripting: which Python environment does PAD use? By default, it uses whatever python resolves to on the PATH. In a conda or venv-heavy shop, this might not be the environment with your required libraries. You can work around this by calling the specific interpreter explicitly in your script preamble, but PAD's Python action doesn't let you configure the interpreter path — it calls the system Python.
For unattended deployments, the service account's PATH environment may differ from an interactive user's. Test explicitly with the service account context.
Variable injection works identically to PowerShell — PAD replaces %VariableName% tokens in the script body. The output mechanism is also the same: capture stdout.
import json
import os
from datetime import datetime, timedelta
# Injected PAD variables
report_dir = r'%ReportDirectory%'
days_lookback = %DaysLookback%
# Find recent files
cutoff = datetime.now() - timedelta(days=days_lookback)
recent_files = []
for fname in os.listdir(report_dir):
if not fname.endswith('.csv'):
continue
fpath = os.path.join(report_dir, fname)
mtime = datetime.fromtimestamp(os.path.getmtime(fpath))
if mtime > cutoff:
size_kb = round(os.path.getsize(fpath) / 1024, 1)
recent_files.append({
'name': fname,
'modified': mtime.strftime('%Y-%m-%d %H:%M:%S'),
'size_kb': size_kb
})
print(json.dumps(recent_files))
Note the use of a raw string (r'%ReportDirectory%') for the path. This prevents Windows backslashes from being misinterpreted as escape sequences. That said, the cleaner pattern is to normalize the path inside Python:
import os
report_dir = os.path.normpath('%ReportDirectory%')
One of Python's biggest advantages is library access. Here's a realistic scenario: you're processing a CSV that has been exported by a legacy system and needs reshaping before it can be loaded into a database. Rather than wrestling with PAD's data table actions or running an Excel macro, you do it in Python:
import pandas as pd
import json
input_path = r'%InputCSVPath%'
output_path = r'%OutputCSVPath%'
# Load the messy export
df = pd.read_csv(
input_path,
skiprows=3, # Legacy system dumps 3 header rows
encoding='latin-1', # Classic enterprise encoding
dtype=str # Read everything as string first
)
# Normalize column names
df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
# Drop completely empty rows
df.dropna(how='all', inplace=True)
# Clean numeric columns
df['amount'] = pd.to_numeric(df['amount'].str.replace(',', '').str.strip(), errors='coerce')
df['quantity'] = pd.to_numeric(df['quantity'].str.strip(), errors='coerce')
# Filter and aggregate
summary = df.groupby('product_code').agg(
total_amount=('amount', 'sum'),
total_qty=('quantity', 'sum'),
record_count=('amount', 'count')
).reset_index()
# Write output
summary.to_csv(output_path, index=False)
# Report back to PAD
result = {
'rows_processed': len(df),
'products_found': len(summary),
'output_path': output_path
}
print(json.dumps(result))
This is exactly the kind of work where Python scripting inside PAD pays off: complex data transformation that would be dozens of PAD actions done in twenty lines of pandas.
Key insight
When you use pandas or other data-science libraries inside PAD scripts, you're effectively embedding a lightweight ETL capability into your RPA flow. This is powerful, but it creates a maintenance dependency — the Python environment on the bot machine must stay synchronized with your script's requirements. Pin your library versions in a requirements.txt and include environment validation in your deployment process.
Python's print() goes to stdout, and everything in stdout becomes the PAD output variable. If you call print() multiple times, the output variable contains multiple lines joined by newlines. PAD doesn't magically parse these — your output variable is a single text block. Design your scripts to produce a single, structured JSON output as the last print() call. Earlier print() calls can be used for logging (they'll appear in the output variable, which you can inspect during debugging) but be aware they'll contaminate your output if you try to parse it as JSON.
A cleaner pattern for debug logging without contaminating output:
import sys
def log(message):
print(message, file=sys.stderr)
# Debug statements go to stderr (captured in PAD's error output variable)
log(f"Processing file: {input_path}")
# Final structured result goes to stdout (captured in PAD's output variable)
print(json.dumps(result))
VBScript feels like legacy technology — because it is. Microsoft has been deprecating it in stages since Windows 10 22H2 and it's effectively end-of-life on modern Windows. However, VBScript remains relevant in PAD for one specific reason: it has native access to COM objects and the Windows Scripting Host (WSH) object model, and some enterprise environments still rely heavily on COM-based integrations.
If you're dealing with a legacy application that exposes automation through COM, or if you need to drive an application that has VBA but not a REST API, VBScript can access CreateObject() in ways that require significant extra effort in PowerShell.
WScript.Shell, WScript.FileSystem)Same pattern — PAD injects %VariableName% as text substitution. VBScript strings are delimited with double quotes, which creates the same quoting vulnerability as PowerShell:
Dim filePath
filePath = "%FilePath%"
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
If fso.FileExists(filePath) Then
Dim f
Set f = fso.GetFile(filePath)
WScript.Echo f.Size
Else
WScript.Echo "FILE_NOT_FOUND"
End If
WScript.Echo sends output to stdout when run under cscript.exe, which is what PAD uses. The output goes into PAD's output variable.
Here's a scenario where VBScript earns its place: you need to automate a legacy Windows application that exposes a COM automation interface but has no modern API. The application vendor provided VBScript examples in their documentation, and rewriting that in PowerShell's COM interop syntax would take longer than just using VBScript:
Dim app
Dim report
Dim exportPath
exportPath = "%ExportPath%"
On Error GoTo ErrorHandler
' Connect to running instance or start new one
Set app = GetObject(, "LegacyApp.Application")
' Navigate to the report module
Set report = app.Reports.Item("%ReportName%")
' Configure and export
report.DateFrom = "%StartDate%"
report.DateTo = "%EndDate%"
report.Export(exportPath, 1) ' 1 = PDF format
WScript.Echo "SUCCESS:" & exportPath
Set report = Nothing
Set app = Nothing
WScript.Quit 0
ErrorHandler:
WScript.Echo "ERROR:" & Err.Description
WScript.Quit 1
Note the error handling pattern: write a prefix string to stdout (SUCCESS: or ERROR:) and check for it in PAD using a Text starts with condition. This is VBScript's version of the structured JSON output pattern from PowerShell and Python — crude, but effective.
Note
VBScript's On Error GoTo syntax is a label-based jump, and in VBScript the correct syntax is actually On Error Resume Next with If Err.Number <> 0 Then checks. The GoTo style above doesn't work in VBScript the way it does in VBA. In production, use On Error Resume Next and check Err.Number after each risky operation.
The DOS script action is the simplest: it runs a batch file in a cmd.exe environment. Use it when you need to run command-line tools, invoke existing batch scripts maintained by your infrastructure team, or chain together CLI operations.
@echo off
setlocal enabledelayedexpansion
set SOURCE_DIR=%SourceDirectory%
set DEST_DIR=%DestinationDirectory%
set ARCHIVE_NAME=%ArchiveName%
REM Use 7-Zip to compress a directory
"C:\Program Files\7-Zip\7z.exe" a -tzip "%DEST_DIR%\%ARCHIVE_NAME%.zip" "%SOURCE_DIR%\*" -r
if %ERRORLEVEL% EQU 0 (
echo SUCCESS:%DEST_DIR%\%ARCHIVE_NAME%.zip
) else (
echo ERROR:7-Zip returned error code %ERRORLEVEL%
exit /b 1
)
Batch scripting has the same variable injection pattern, but there's a collision risk: % is also the batch variable delimiter. When PAD injects %SourceDirectory%, it replaces the entire token including the percent signs. But if your batch script uses native batch variables like %ERRORLEVEL%, PAD will try to substitute those too — and since ERRORLEVEL isn't a PAD variable, the substitution results in an empty string, breaking the script.
The workaround: use %% for literal percent signs in batch syntax. So your batch variables should be %%ERRORLEVEL%%, %%i in for loops, etc. The double-percent escaping tells PAD's substitution engine to output a single %, which the batch interpreter then sees normally.
@echo off
for %%f in ("%SourceDirectory%\*.csv") do (
echo Processing: %%f
)
This is ugly, but it's the correct pattern.
Tip
For anything beyond simple tool invocation, prefer PowerShell over batch. Batch scripting's variable scoping, error handling, and string manipulation are notoriously fragile. Reserve the DOS script action for cases where you're integrating with existing .bat files your organization already owns and maintains.
A common design pattern is to use one scripting block to gather data, pass it through PAD variables to transformation logic or UI automation steps, then use another scripting block to write results back. This keeps each block focused and testable.
For example, in a flow that processes daily sales data:
This architecture keeps the "talking to Windows" work in PAD's native actions and the "complex data manipulation" work in script blocks. Each has clear inputs and outputs.
PAD's variable substitution is text-based, which means passing a data table into a script requires serialization. The practical approach is to first use PAD actions to write the data table to a temporary CSV file, then have the script read that file. This avoids all the quoting and escaping headaches of trying to inject tabular data as text.
# In PAD flow:
1. Write Data Table to CSV action -> temp file at %TempPath%
2. Run Python Script:
- Injects %TempPath% into the script
- Script reads the CSV, processes it, writes output CSV
3. Read CSV action -> load result back into PAD data table
4. Delete File action -> clean up temp file
This file-mediated pattern is robust and easy to debug — you can inspect the temp files if something goes wrong.
If your flow needs to call external services from inside a script, you should retrieve credentials from a secure store rather than hardcoding them in the script or injecting them from plaintext PAD variables. Handling credentials securely in desktop flows covers the full approach. The pattern in script context looks like this:
# Retrieve the password from Windows Credential Manager rather than from PAD variable
$cred = Get-StoredCredential -Target "MyDatabaseConnection"
$connectionString = "Server=%SQLServer%;Database=%DatabaseName%;User Id=$($cred.Username);Password=$($cred.GetNetworkCredential().Password);"
# The server name and database name come from PAD (low-sensitivity config)
# Only credentials come from the secure store
This keeps the sensitive bits out of PAD's variable system entirely while still allowing PAD to pass the non-sensitive configuration.
A scripting action in PAD is a text field. There's no version control integration, no syntax highlighting, no diff capability. This creates real maintenance problems as scripts grow complex. Here are the patterns that work at scale.
The PAD scripting action is excellent for glue code — a dozen lines that call an external script file containing the real logic. This way, the complex logic lives in a .ps1 or .py file that can be version-controlled, tested independently, and deployed via your normal CI/CD process.
# PAD's PowerShell action - just the orchestration layer
$scriptPath = "%ScriptRepository%\transform_orders.ps1"
$params = @{
InputPath = "%InputPath%"
OutputPath = "%OutputPath%"
Environment = "%Environment%"
}
$result = & $scriptPath @params | ConvertFrom-Json
Write-Output ($result | ConvertTo-Json -Compress)
The actual transform_orders.ps1 lives in a monitored repository, goes through code review, and gets deployed to a known path on bot machines. The PAD flow just calls it.
This pattern integrates cleanly with subflows and reusable logic in Power Automate Desktop — you can put the "call external script" pattern into a reusable subflow that accepts the script path and parameters as inputs.
Since script blocks don't have a name field (you can add a comment at the action level in PAD, but it's not obvious), add a structured comment header to every script block:
<#
.PURPOSE
Query SQL Server for unprocessed orders from the past 24 hours.
Returns JSON array of order objects.
.INPUTS
%SQLServer% - SQL Server hostname or instance
%DatabaseName% - Target database name
.OUTPUTS
JSON array: [{OrderId, CustomerCode, Amount, CreatedAt}]
.LAST_MODIFIED
2024-01-15 - Added null check for CustomerCode field
#>
This is the single most effective thing you can do to make scripting blocks maintainable. When someone opens this flow six months later, they don't have to reverse-engineer what the script does.
This section deserves serious attention. Scripting actions are the most powerful and most dangerous capability in PAD. They execute arbitrary code in the context of the bot's service account, with access to everything that account can reach.
In an unattended RPA deployment, the bot runs as a service account. If that service account has broad network access (common in enterprises where "it needs to access everything"), then a PowerShell script in a PAD flow can reach any network resource, read or write any accessible file share, query any database the account has access to, or exfiltrate data.
If a malicious actor gains control of a flow — through a compromised Power Platform environment, for example — the scripting actions are their most direct path to doing damage. The flow's built-in UI automation actions are limited by application-level access controls; a PowerShell script can bypass many of those.
Mitigation strategies:
Because PAD does naive text substitution, any PAD variable that's injected into a script is a potential code injection vector if that variable value comes from an external, untrusted source. Consider a flow that reads a customer name from a web form and injects it into a PowerShell script:
# DANGEROUS: CustomerName comes from a web form
$query = "SELECT * FROM orders WHERE customer = '%CustomerName%'"
If %CustomerName% contains '; DROP TABLE orders; --, you have a SQL injection vulnerability, executing in the context of the bot's database account. This isn't hypothetical — it's the same class of vulnerability that breaks web applications, just in RPA context.
Always sanitize injected variables before using them in scripts that construct dynamic queries, file paths, or system commands. The file-mediated pattern (write to CSV, read from script) eliminates this vector for data payloads. For configuration values, validate them against an allowlist at the top of every script.
Warning
If your desktop flow takes any inputs from cloud triggers, user input, or external data sources and passes them into scripting actions, you have a code injection risk. Review triggering desktop flows from cloud flows with this risk in mind and sanitize all external inputs before injecting them into scripts.
Script failures need to surface cleanly in PAD's error handling system. There are two mechanisms available and you should use both.
A script that exits with a non-zero exit code causes the PAD scripting action to throw an error, which can be caught with the On block error mechanism. Ensure every production script has explicit exit code handling:
# At the end of a successful PowerShell script
exit 0
# On failure (in catch block)
exit 1
In Python:
import sys
sys.exit(0) # Success
sys.exit(1) # Failure
For cases where you need more nuance — distinguishing between "file not found" and "database connection failed" — return a structured result object and inspect it in PAD:
$result = @{
Success = $true
ErrorCode = $null
Data = $null
}
try {
# Do work
$result.Data = Get-SomeData
}
catch [System.IO.FileNotFoundException] {
$result.Success = $false
$result.ErrorCode = "FILE_NOT_FOUND"
exit 1
}
catch [System.Data.SqlClient.SqlException] {
$result.Success = $false
$result.ErrorCode = "DB_CONNECTION_FAILED"
exit 1
}
Write-Output ($result | ConvertTo-Json -Compress)
exit 0
In PAD, after the scripting action, use a Run PowerShell Script > On error block to handle the exit code failure, and also parse the JSON output when successful to check ErrorCode before proceeding. This gives you both the coarse-grained PAD error handling and the fine-grained application-level error handling.
Scenario: Your company runs a legacy order management system that exports daily transaction files to a shared drive in a malformed CSV format. The export has three header rows of junk, inconsistent date formats, and amounts formatted with regional thousand separators. You need to build a PAD desktop flow that:
Step 1: PowerShell — Find the Latest File
Add a Run PowerShell Script action with this code (adapt paths for your environment):
$ErrorActionPreference = 'Stop'
try {
$exportDir = "%ExportDirectory%"
$latestFile = Get-ChildItem $exportDir -Filter "orders_*.csv" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($null -eq $latestFile) {
Write-Output '{"Success":false,"Error":"No export files found","FilePath":null}'
exit 1
}
$result = @{
Success = $true
FilePath = $latestFile.FullName
FileName = $latestFile.Name
LastModified = $latestFile.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
SizeKB = [math]::Round($latestFile.Length / 1KB, 1)
}
Write-Output ($result | ConvertTo-Json -Compress)
exit 0
}
catch {
Write-Output "{`"Success`":false,`"Error`":`"$($_.Exception.Message)`",`"FilePath`":null}"
exit 1
}
Set the output variable to %PSResult%. After this action, parse the JSON: use a Convert custom object from JSON action on %PSResult%, then access %JsonObj.FilePath%.
Step 2: Python — Normalize the Data
Add a Run Python Script action:
import pandas as pd
import json
import os
import sys
from datetime import datetime
def log(msg):
print(msg, file=sys.stderr)
input_path = r'%InputFilePath%'
output_dir = r'%OutputDirectory%'
try:
log(f"Reading file: {input_path}")
# Skip 3 junk header rows, use 4th row as column headers
df = pd.read_csv(
input_path,
skiprows=3,
encoding='latin-1',
dtype=str
)
original_count = len(df)
# Normalize column names
df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
# Drop completely empty rows
df.dropna(how='all', inplace=True)
# Clean amount: remove thousand separators, convert to float
df['amount'] = (df['amount']
.str.strip()
.str.replace('.', '', regex=False) # European thousands separator
.str.replace(',', '.', regex=False) # European decimal
.pipe(pd.to_numeric, errors='coerce'))
# Normalize dates (handles both MM/DD/YYYY and DD-MM-YYYY)
df['order_date'] = pd.to_datetime(df['order_date'].str.strip(),
dayfirst=True, errors='coerce')
df['order_date'] = df['order_date'].dt.strftime('%Y-%m-%d')
# Filter valid rows
df_clean = df.dropna(subset=['amount', 'order_date'])
rejected_count = original_count - len(df_clean)
# Write output
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_path = os.path.join(output_dir, f'orders_clean_{timestamp}.csv')
df_clean.to_csv(output_path, index=False)
result = {
'success': True,
'input_rows': original_count,
'output_rows': len(df_clean),
'rejected_rows': rejected_count,
'output_path': output_path
}
print(json.dumps(result))
except Exception as e:
result = {'success': False, 'error': str(e)}
print(json.dumps(result))
sys.exit(1)
Step 3: DOS Script — Write the Completion Log
Add a Run DOS Script action. Note the %% escaping for batch variables:
@echo off
set LOGFILE=%LogDirectory%\automation_log.txt
set TIMESTAMP=%LogTimestamp%
set SUMMARY=%CompletionSummary%
echo [%%TIMESTAMP%%] %%SUMMARY%% >> "%%LOGFILE%%"
if %%ERRORLEVEL%% EQU 0 (
echo SUCCESS
) else (
echo ERROR: Could not write to log
exit /b 1
)
Before this step, in PAD, use a Get current date and time action to set %LogTimestamp%, and format %CompletionSummary% using a Set variable action that concatenates the row counts from the Python output.
This exercise demonstrates the full pattern: PowerShell for Windows-native discovery, Python for complex data transformation, and DOS for simple system-level logging — each doing what it does best.
"My script works in PowerShell ISE but fails in PAD"
The most common cause is the execution context. Your ISE session has your user profile loaded, your mapped drives available, and your PATH configured. PAD's spawned process may have none of these. Test your scripts by opening a fresh cmd.exe or PowerShell window as the bot service account (use runas or PSExec) and running them from that context.
"The output variable is empty even though the script ran"
Check whether your script is writing to stderr instead of stdout. Many cmdlets and tools default to stderr for informational messages. Write-Error, Write-Verbose, Write-Warning, and Write-Debug in PowerShell all go to different streams, not stdout. Only Write-Output, Write-Host (in some contexts), and pipeline output go to stdout.
"PAD says the action succeeded but the output looks wrong"
PAD considers the scripting action successful if the exit code is 0 (or if the script didn't explicitly set an exit code). A script can exit cleanly with incorrect output — particularly if your try/catch silently swallows exceptions and outputs an empty result. Always include an explicit success/failure flag in your JSON output and check it in PAD.
"My script has percent signs in it and they're being replaced with empty strings"
This is the PAD variable substitution collision described earlier. For PowerShell, use $var for all internal variables and only use %PadVar% tokens for PAD input injection. For batch scripts, double up percent signs to %%.
"The Python script can't find the pandas module"
PAD is calling a different Python than you think. Add this diagnostic at the top of your Python script to capture which Python PAD is actually using:
import sys
import json
print(json.dumps({
'python_executable': sys.executable,
'python_version': sys.version,
'path': sys.path
}))
Run this script in PAD and inspect the output. Then install your required packages into that specific Python environment.
"My VBScript works on my machine but fails on the bot machine"
COM objects are machine-specific registrations. The COM server must be registered on the bot machine (using regsvr32 or the application's installer), and the bot service account must have DCOM permission to access it. Check the Component Services console (dcomcnfg.exe) on the bot machine.
Tip
For complex debugging scenarios, write a timestamped debug log inside your script to a temp file. After the PAD action runs (whether it fails or succeeds), add a PAD Read text from file action to load and store the debug log in a variable, then use a Write to event log or similar action to persist it. This gives you post-mortem visibility that the PAD output variable alone doesn't provide.
Scripting actions are the bridge between PAD's declarative automation layer and the full power of Windows compute. PowerShell gives you native Windows system access and excellent JSON output handling. Python gives you data science libraries and complex text processing. VBScript gives you COM object access for legacy systems. Batch gives you simple tool invocation and integration with existing scripts.
The patterns that make scripting actions maintainable and safe are consistent across all four languages: inject variables at the top and sanitize them immediately, return structured JSON output, use explicit exit codes to signal success or failure, apply least-privilege to bot service accounts, and keep complex logic in external versioned files rather than inline in PAD's text fields.
Where you go from here depends on your automation landscape. If you're building flows that span cloud triggers and desktop execution, review triggering desktop flows from cloud flows to understand how inputs flow into your flows from outside. If you're deploying to unattended machines at scale, managing machines and machine groups covers the infrastructure side — including how to ensure Python environments and PowerShell modules are consistently deployed across your bot fleet. And if you're handling sensitive configuration that your scripts need to access, handling credentials securely in desktop flows is required reading before you go to production.
The hardest part of scripting inside PAD isn't writing the script — it's integrating it cleanly with the flow around it. Once you've internalized the variable injection model, the output capture pattern, and the error handling contract, you'll find that scripting actions transform PAD from a UI automation tool into a general-purpose automation platform.