Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Automate

Scripting Inside Desktop Flows: Running PowerShell, Python, and VBScript Actions

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.

🔥 Expert30 min readSep 22, 2026Updated Sep 22, 2026
Scripting Inside Desktop Flows: Running PowerShell, Python, and VBScript Actions
On this page
  • Introduction
  • Prerequisites
  • How PAD Executes Scripts: The Architecture You Need to Understand
  • The Run PowerShell Script Action
  • Setting Up the Action
  • Passing Variables Into PowerShell
  • Returning Data Back to PAD
  • Handling PowerShell Errors
  • PowerShell Execution Policy Considerations
  • The Run Python Script Action
  • Python Runtime Requirements
  • Variable Injection and Output in Python
  • Using pandas and numpy Inside PAD
  • Returning Multi-Line Output
  • The Run VBScript Action
  • When VBScript Still Makes Sense
  • Variable Injection in VBScript
  • A Practical COM Automation Example
  • The Run DOS Script Action (CMD/Batch)
  • Practical Use Cases
  • Advanced Integration Patterns
  • Chaining Script Actions with Intermediate Variables
  • Passing Lists and Data Tables Into Scripts
  • Using Scripts to Extend PAD's Credential Handling
  • Organizing Scripts for Maintainability
  • Keep Scripts Short; Call External Files for Complex Logic
  • Documentation Inside Script Blocks
  • Security Implications of Scripting Actions
  • What the Bot Account Can Do
  • Code Injection via Variable Injection
  • Error Handling Integration
  • Exit Codes
  • Structured Output Inspection
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Scripting Inside Desktop Flows: Running PowerShell, Python, and VBScript Actions

    Introduction

    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:

    • How PAD's PowerShell, Python, VBScript, and DOS script actions work under the hood, including process isolation and variable injection
    • Techniques for passing PAD variables into scripts and returning complex outputs back into the flow
    • When to choose each scripting language and the architectural trade-offs involved
    • How to handle errors from scripting blocks and integrate them with PAD's error handling system
    • Security considerations for scripting in attended vs. unattended deployments

    Prerequisites

    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.


    How PAD Executes Scripts: The Architecture You Need to Understand

    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:

    1. Serializes any injected variables into the script body as string substitution
    2. Spawns a child process using the appropriate interpreter (powershell.exe, python.exe, cscript.exe, or cmd.exe)
    3. Writes the script to a temporary file and executes it
    4. Captures stdout as the primary return channel
    5. Maps captured output to the designated output variable

    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.


    The Run PowerShell Script Action

    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.

    Setting Up the Action

    In the PAD designer, find the action under Scripting > Run PowerShell Script. The action has these key fields:

    • PowerShell code: The script body, which supports PAD variable injection
    • PowerShell variables get: An output variable (type: Text) that will capture everything written to stdout
    • PowerShell script error output: A separate variable that captures stderr

    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.

    Passing Variables Into PowerShell

    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.

    Returning Data Back to PAD

    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.

    Handling PowerShell Errors

    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.

    PowerShell Execution Policy Considerations

    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.


    The Run Python Script Action

    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.

    Python Runtime Requirements

    Unlike PowerShell (which ships with Windows), Python must be installed on the machine separately. PAD does not bundle a Python runtime. You need:

    • Python 3.x installed and accessible from the PATH (or the executable path configured in PAD's settings)
    • Any required third-party libraries installed in the Python environment that PAD's service account can access

    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 and Output in Python

    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%')
    

    Using pandas and numpy Inside PAD

    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.

    Returning Multi-Line Output

    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))
    

    The Run VBScript Action

    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.

    When VBScript Still Makes Sense

    • Driving old COM servers (legacy SCADA systems, ancient accounting software, custom ActiveX components)
    • Scripts that were already written in VBScript and just need to be embedded in PAD without rewriting
    • Environments where PowerShell is restricted by policy but VBScript/cscript.exe isn't
    • Interacting with Windows Script Host object model (WScript.Shell, WScript.FileSystem)

    Variable Injection in VBScript

    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.

    A Practical COM Automation Example

    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 Run DOS Script Action (CMD/Batch)

    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.

    Practical Use Cases

    @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.


    Advanced Integration Patterns

    Chaining Script Actions with Intermediate Variables

    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:

    1. PowerShell block: Query a SQL Server database, return JSON with today's unprocessed orders
    2. PAD For each loop: Iterate over the orders list
    3. PAD UI automation: Enter each order into a legacy web application (see web automation actions)
    4. Python block: After all orders are entered, reshape the completion report and write it to a SharePoint-accessible file share

    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.

    Passing Lists and Data Tables Into Scripts

    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.

    Using Scripts to Extend PAD's Credential Handling

    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.


    Organizing Scripts for Maintainability

    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.

    Keep Scripts Short; Call External Files for Complex Logic

    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.

    Documentation Inside Script Blocks

    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.


    Security Implications of Scripting Actions

    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.

    What the Bot Account Can Do

    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:

    • Apply least-privilege to the bot service account. It should have access to exactly the resources the automation needs and nothing more.
    • Use PAD's sensitive variable types for any credentials that must flow through PAD, and retrieve other credentials from Azure Key Vault or Windows Credential Manager.
    • Audit what scripts your flows contain as part of your change management process. Treat changes to scripting blocks with the same rigor as infrastructure changes.
    • In regulated environments, consider whether scripting actions should be available at all — Power Platform DLP policies don't currently restrict scripting action content, so governance is a people-and-process problem, not a platform one.

    Code Injection via Variable Injection

    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.


    Error Handling Integration

    Script failures need to surface cleanly in PAD's error handling system. There are two mechanisms available and you should use both.

    Exit Codes

    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
    

    Structured Output Inspection

    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.


    Hands-On Exercise

    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:

    1. Discovers the most recent export file using a PowerShell script
    2. Processes and normalizes the data using a Python script
    3. Writes the clean output to a new location
    4. Logs a completion summary to a text file using a DOS batch command

    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.


    Common Mistakes & Troubleshooting

    "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.


    Summary & Next Steps

    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.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Power Automate Desktop & RPA

    Previous

    Automating File, Folder, and Email Operations with Power Automate Desktop

    Next

    Monitoring and Troubleshooting Desktop Flow Runs at Scale

    Related Insights

    Power AutomateFoundation

    Automating Windows Clipboard Operations in Power Automate Desktop: Copying, Pasting, and Transferring Data Between Applications Without UI Interaction

    18 min
    Power AutomateExpert

    Building a Resilient Unattended RPA Orchestration Framework in Power Automate Desktop: Queue-Driven Job Dispatch, Machine Load Balancing, and Automated Recovery for 24/7 Production Bot Fleets

    30 min
    Power AutomatePractitioner

    Automating Internet Explorer and Citrix-Hosted Applications in Power Automate Desktop: Selector Strategies, Session Management, and Reliable Data Extraction from Virtual Environments

    25 min

    On this page

    • Introduction
    • Prerequisites
    • How PAD Executes Scripts: The Architecture You Need to Understand
    • The Run PowerShell Script Action
    • Setting Up the Action
    • Passing Variables Into PowerShell
    • Returning Data Back to PAD
    • Handling PowerShell Errors
    • PowerShell Execution Policy Considerations
    • The Run Python Script Action
    • Python Runtime Requirements
    • Variable Injection and Output in Python
    • Using pandas and numpy Inside PAD
    • Returning Multi-Line Output
    • The Run VBScript Action
    • When VBScript Still Makes Sense
    • Variable Injection in VBScript
    • A Practical COM Automation Example
    • The Run DOS Script Action (CMD/Batch)
    • Practical Use Cases
    • Advanced Integration Patterns
    • Chaining Script Actions with Intermediate Variables
    • Passing Lists and Data Tables Into Scripts
    • Using Scripts to Extend PAD's Credential Handling
    • Organizing Scripts for Maintainability
    • Keep Scripts Short; Call External Files for Complex Logic
    • Documentation Inside Script Blocks
    • Security Implications of Scripting Actions
    • What the Bot Account Can Do
    • Code Injection via Variable Injection
    • Error Handling Integration
    • Exit Codes
    • Structured Output Inspection
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Summary & Next Steps