Six months ago I spent a lot of time trying to get PowerShell Workflows running but eventually gave up in frustration. Now I've revisited the issue in PowerShell 5 and found the root cause seems to be that Write-Verbose and Write-Debug can cause untrapped (and unexplained) early terminations and that the behaviour is almost arbitrary depending on exactly how and when they are called.

Let's start with a sample that works; it doesn't illustrate good workflow design because it exists solely to test the problem at hand. Also it has some extra code so that we can quickly expand on it later.

workflow Run-Workflow {
    InlineScript {
        function Func1 {
            Write-Verbose "In Func1"
            
			try {
            		$something = "Something"
            		Write-Verbose "Created a $($something.GetType())"
            		Func2 $something
			} catch {
               		Write-Verbose "Oops; $_"
			}
        }

        function Func2 {
            param ($Something)

            Write-Verbose "In Func2; received a $($Something.GetType())"
        }

        # This little dance is to avoid Import-Module spam when -Verbose is specified
        $VerbosePreference = "SilentlyContinue"
        Import-Module SqlPs
        $VerbosePreference = "Continue"
        Func1
    }
}

Run-Workflow -Verbose

This is what it's doing:

  1. We're defining and calling a workflow.
  2. The workflow calls a function.
  3. The function creates and object and passes it to another function.
  4. Everything is written verbosely.

These functions don't have to be defined here; they can be in a module anywhere and then called by the workflow. Here's the output.

VERBOSE: [localhost]:In Func1
VERBOSE: [localhost]:Created a string
VERBOSE: [localhost]:In Func2; received a string

The theory written on all the PowerShell blogs is that an InlineScript is a lot like its own PowerShell process (actually a runspace) which runs code separately then returns output to the workflow. But this explanation must be overly simplified because with a tiny bit of complexity it will soon fail spectacularly.

Now I'm going to make one change; the kind of object I'm passing between functions. You don't need an active SQL Server instance to test this.

workflow Run-Workflow {
    InlineScript {
        function Func1 {
            Write-Verbose "In Func1"
            
            try {
                ##########
                $something = New-Object Microsoft.SqlServer.Management.Smo.Server "."
                Write-Verbose "Created a $($Something.GetType())"
                Func2 $something
            } catch {
                Write-Verbose "Oops; $_"
            }
        }

        function Func2 {
            param ($Something)
            Write-Verbose "In Func2; received a $($Something.GetType())"
        }

        # This little dance is to avoid Import-Module spam when -Verbose is specified
        $VerbosePreference = "SilentlyContinue"
        Import-Module SqlPs
        $VerbosePreference = "Continue"
        Func1
    }
}

Run-Workflow -Verbose

And here's the output.

VERBOSE: [localhost]:In Func1
VERBOSE: [localhost]:Created a Microsoft.SqlServer.Management.Smo.Server
Object reference not set to an instance of an object.
    + CategoryInfo          : ResourceUnavailable: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : System.Management.Automation.Remoting.PSRemotingDataStructureException
    + PSComputerName        : [localhost]

Now I know what you're thinking! "That took a few seconds, it's some kind of serialization!" Okay, ignore that the workflow aborted completely, that it couldn't be trapped, and that serialization is not meant to be occurring within an InlineScript. But I'm going to do exactly the same thing using Write-Host instead of Write-Verbose and this time it's going to work.

workflow Run-Workflow {
    InlineScript {
        function Func1 {
            Write-Verbose "In Func1"
            
            try {
                $something = New-Object Microsoft.SqlServer.Management.Smo.Server "."
                Write-Verbose "Created a $($Something.GetType())"
                Func2 $something
            } catch {
                Write-Verbose "Oops; $_"
            }
        }

        function Func2 {
            param ($Something)

            ##########
            Write-Host "In Func2; received a $($Something.GetType())"
        }

        # This little dance is to avoid Import-Module spam when -Verbose is specified
        $VerbosePreference = "SilentlyContinue"
        Import-Module SqlPs
        $VerbosePreference = "Continue"
        Func1
    }
}

Run-Workflow -Verbose
VERBOSE: [localhost]:In Func1
VERBOSE: [localhost]:Created Microsoft.SqlServer.Management.Smo.Server
In Func2; received a Microsoft.SqlServer.Management.Smo.Server

If I may also take a guess at what you're thinking, it's that Write-Verbose is triggering some weird kind of problem, that you can call it in some functions and not others? Well it's going to get even weirder because I'm going to hide Write-Verbose within a function which calls Write-Verbose and it will all work fine!

workflow Run-Workflow {
    InlineScript {
        function Func1 {
            Write-Verbose "In Func1"
            
            try {
                $something = New-Object Microsoft.SqlServer.Management.Smo.Server "."
                Write-Verbose "Created a $($Something.GetType())"
                Func2 $something
            } catch {
                Write-Verbose "Oops; $_"
            }
        }

        function Func2 {
            param ($Something)

            ########################
            function Write-Verbose {
		        $args | Microsoft.PowerShell.Utility\Write-Verbose
            }

            Write-Verbose "In Func2; received a $($Something.GetType())"
        }

        # This little dance is to avoid Import-Module spam when -Verbose is specified
        $VerbosePreference = "SilentlyContinue"
        Import-Module SqlPs
        $VerbosePreference = "Continue"
        Func1
    }
}

Run-Workflow -Verbose
VERBOSE: [localhost]:In Func1
VERBOSE: [localhost]:Created a Microsoft.SqlServer.Management.Smo.Server
VERBOSE: In Func2; received a Microsoft.SqlServer.Management.Smo.Server

That's inexplicable.

One thing which does make it all work is setting $PSRunInProcessPreference which, "If this variable is specified, all activities in the enclosing scope are run in the workflow process." Unfortunately that doesn't explain what's really going on and what the impacts are, so I won't use it. But here it is turning the original failing script into a working one.

workflow Run-Workflow {
    $PSRunInProcessPreference = $true

    InlineScript {
        function Func1 {
            Write-Verbose "In Func1"
            
            try {
                $something = New-Object Microsoft.SqlServer.Management.Smo.Server "."
                Write-Verbose "Created a $($Something.GetType())"
                Func2 $something
            } catch {
                Write-Verbose "Oops; $_"
            }
        }

        function Func2 {
            param ($Something)
            Write-Verbose "In Func2; received a $($Something.GetType())"
        }

        # This little dance is to avoid Import-Module spam when -Verbose is specified
        $VerbosePreference = "SilentlyContinue"
        Import-Module SqlPs
        $VerbosePreference = "Continue"
        Func1
    }
}

Run-Workflow -Verbose
VERBOSE: [localhost]:In Func1
VERBOSE: [localhost]:Created a Microsoft.SqlServer.Management.Smo.Server
VERBOSE: [localhost]:In Func2; received a Microsoft.SqlServer.Management.Smo.Server

But WHY?

I spent a long time trying to work out what's going on with this and have come up with nothing, except a feeling of exhaustion and vindication that whatever I tried to do was being tripped up by PowerShell workflows; I guess other people are using it for trivial work and not passing objects between functions while outputting debugging information.

I'd love an answer.