Basic Tasks

Run ad-hoc scripts as background tasks with status, progress, results, cancellation, and controlled removal.

Full source

File: pwsh/tutorial/examples/20.1-Task.ps1

<#
    Sample Kestrun Server Configuration – Tasks Demo
    This script shows how to enable the Tasks feature and use it via HTTP routes.
    FileName: 20.1-Task.ps1
#>

param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)

# Configure default logging
New-KrLogger |
    Set-KrLoggerLevel -Value Debug |
    Add-KrSinkConsole |
    Register-KrLogger -Name 'myLogger' -SetAsDefault

# Create a new Kestrun server
New-KrServer -Name 'Tasks Demo Server'

# Listener
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -SelfSignedCert

# --- Tasks setup ------------------------------------------------------------
# Register the ad-hoc Tasks feature (PowerShell, C#, VB.NET)
Add-KrTasksService

# Enable configuration
Enable-KrConfiguration

# --- Routes -----------------------------------------------------------------

# 1) Create a PowerShell task (does NOT start it). Example:
#    /tasks/create/ps?seconds=2
Add-KrMapRoute -Verbs Get -Path '/tasks/create/ps' -ScriptBlock {
    $seconds = Get-KrRequestQuery -Name 'seconds' -AsInt
    if ($seconds -le 0) { $seconds = 2 }
    Write-KrLog -Level Debug -Message 'Creating PS task that sleeps for {seconds} seconds' -Values $seconds

    # Create a new PowerShell task that sleeps for the specified seconds and reports progress
    $id = New-KrTask -ScriptBlock {
        Write-Debug "Task started, will sleep for $sec seconds"
        if ($sec -lt 1) { $sec = 1 }
        for ($i = 1; $i -le $sec; $i++) {
            if ($i -lt $sec) {
                $TaskProgress.StatusMessage = "Sleeping ($i/$sec)"
                $TaskProgress.PercentComplete = [int](($i - 1) * 100 / $sec)
            }
            Start-Sleep -Seconds 1
        }
        $TaskProgress.Complete('Completed')
        'PS task done at ' + (Get-Date).ToString('o')
    } -Arguments @{ 'sec' = $seconds }
    Write-KrJsonResponse -StatusCode 200 -InputObject @{ id = $id; language = 'PowerShell' }
}

# 2) Create a C# task (does NOT start it). Example:
#    /tasks/create/cs?ms=1500
Add-KrMapRoute -Verbs Get -Path '/tasks/create/cs' -ScriptBlock {
    $ms = [int](Get-KrRequestQuery -Name 'ms')
    if ($ms -le 0) { $ms = 1500 }

    $code = @'
var steps = 5;
for (int i = 1; i <= steps; i++)
{
    var pct = (i - 1) * 100 / steps;
         TaskProgress.StatusMessage = $"Working {i}/{steps}";
         TaskProgress.PercentComplete = pct;
    await Task.Delay(total / steps);
}
    TaskProgress.Complete("Completed");
"CS task done at " + DateTime.UtcNow.ToString("o")
'@

    $id = New-KrTask -Language CSharp -Code $code -Arguments @{ 'total' = $ms }
    Write-KrJsonResponse -StatusCode 200 -InputObject @{ id = $id; language = 'CSharp' }
}

# 3) Start a previously created task. Example:
#    /tasks/start?id=<taskId>
Add-KrMapRoute -Verbs Get -Path '/tasks/start' -ScriptBlock {
    $id = Get-KrRequestQuery -Name 'id'
    if ([string]::IsNullOrWhiteSpace($id)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{ error = "Missing 'id' query parameter" }
        return
    }
    $ok = Start-KrTask -Id $id
    $status = if ($ok) { 202 } else { 409 }
    Write-KrJsonResponse -StatusCode $status -InputObject @{ started = $ok; id = $id }
}

# 4) Get task state. Example:
#    /tasks/state?id=<taskId>
Add-KrMapRoute -Verbs Get -Path '/tasks/state' -ScriptBlock {
    $id = Get-KrRequestQuery -Name 'id'
    if ([string]::IsNullOrWhiteSpace($id)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{ error = "Missing 'id' query parameter" }
        return
    }
    $task = Get-KrTask -Id $id
    if ($null -eq $task) {
        Write-KrJsonResponse -StatusCode 404 -InputObject @{ error = 'Task not found' }
        return
    }
    Write-KrJsonResponse -StatusCode 200 -InputObject $task
}

# 5) Get detailed result snapshot. Example:
#    /tasks/result?id=<taskId>
Add-KrMapRoute -Verbs Get -Path '/tasks/result' -ScriptBlock {
    $id = Get-KrRequestQuery -Name 'id'
    if ([string]::IsNullOrWhiteSpace($id)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{ error = "Missing 'id' query parameter" }
        return
    }
    $state = Get-KrTaskState -Id $id
    if ( $null -eq $state) {
        Write-KrJsonResponse -StatusCode 404 -InputObject @{ error = 'Task not found' }
        return
    }
    if ($state -ne 'Completed' -and $state -ne 'Stopped' -and $state -ne 'Failed') {
        Write-KrJsonResponse -StatusCode 409 -InputObject @{ error = 'Task is not in a completed state' }
        return
    }
    $r = Get-KrTaskResult -Id $id

    Write-KrJsonResponse -StatusCode 200 -InputObject $r
}

# 6) Cancel a task. Example:
#    /tasks/cancel?id=<taskId>
Add-KrMapRoute -Verbs Get -Path '/tasks/cancel' -ScriptBlock {
    $id = Get-KrRequestQuery -Name 'id'
    if ([string]::IsNullOrWhiteSpace($id)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{ error = "Missing 'id' query parameter" }
        return
    }
    $ok = Stop-KrTask -Id $id
    $status = if ($ok) { 202 } else { 409 }
    Write-KrJsonResponse -StatusCode $status -InputObject @{ cancelled = $ok; id = $id }
}

# 7) Remove a finished task. Example:
#    /tasks/remove?id=<taskId>
Add-KrMapRoute -Verbs Get -Path '/tasks/remove' -ScriptBlock {
    $id = Get-KrRequestQuery -Name 'id'
    if ([string]::IsNullOrWhiteSpace($id)) {
        Write-KrJsonResponse -StatusCode 400 -InputObject @{ error = "Missing 'id' query parameter" }
        return
    }
    $ok = Remove-KrTask -Id $id
    $status = if ($ok) { 200 } else { 404 }
    Write-KrJsonResponse -StatusCode $status -InputObject @{ removed = $ok; id = $id }
}

# 8) List all tasks.
#    /tasks/list
Add-KrMapRoute -Verbs Get -Path '/tasks/list' -ScriptBlock {
    $list = Get-KrTask
    Write-KrJsonResponse -StatusCode 200 -InputObject $list
}

# 9) Convenience: One-shot create+start PS task.
#    /tasks/run/ps?seconds=1
Add-KrMapRoute -Verbs Get -Path '/tasks/run/ps' -ScriptBlock {
    $seconds = [int](Get-KrRequestQuery -Name 'seconds')
    if ($seconds -le 0) { $seconds = 1 }
    $scriptBlock = {
        for (
            $i = 1; $i -le $seconds; $i++
        ) {
            if ($i -lt $seconds) {
                $TaskProgress.StatusMessage = "Sleeping ($i/$seconds)"
                $TaskProgress.PercentComplete = [int](($i - 1) * 100 / $seconds)
            }
            Start-Sleep -Seconds 1
        }
        $TaskProgress.Complete('Completed')
        Get-Date
    }
    # One-shot = create + start
    $id = New-KrTask -ScriptBlock $scriptBlock -Arguments @{ 'seconds' = $seconds }
    $null = Start-KrTask -Id $id
    Write-KrJsonResponse -StatusCode 202 -InputObject @{ id = $id; started = $true }
}

# Convenience: hello
Add-KrMapRoute -Verbs Get -Path '/hello' -ScriptBlock {
    Write-KrTextResponse -StatusCode 200 -InputObject 'Hello, Tasks World!'
}

# Start the server asynchronously
Start-KrServer -CloseLogsOnExit

Step-by-step

  1. Logging: Register a console logger and set level to Debug.
  2. Server: Create a Kestrun server named “Tasks Demo Server”.
  3. Listener: Listen on loopback using a self-signed certificate on the provided port (HTTPS).
  4. Tasks: Enable the ad-hoc task feature with Add-KrTasksService (injects progress state per task).
  5. Configure: Call Enable-KrConfiguration to build the app.
  6. Routes: Map endpoints to create, start (idempotent), query state + progress, fetch result, cancel, remove (child-safe), and list tasks.
  7. Convenience: Add a one-shot route to create+start a PowerShell task.
  8. Start: Run the server asynchronously with Start-KrServer.

Try it

$base = 'https://127.0.0.1:5000'

# Create a PowerShell task (does not start)
curl -k -s "$base/tasks/create/ps?seconds=2" | jq

# Create a C# task (does not start)
curl -k -s "$base/tasks/create/cs?ms=1500" | jq

# Start a task by id
$id = 'REPLACE_WITH_ID'
curl -k -s "$base/tasks/start?id=$id" | jq

# Check state (includes progress) and result (result only)
curl -k -s "$base/tasks/state?id=$id" | jq
curl -k -s "$base/tasks/result?id=$id" | jq

# Cancel and remove
curl -k -s "$base/tasks/cancel?id=$id" | jq
curl -k -s "$base/tasks/remove?id=$id" | jq

# List all
curl -k -s "$base/tasks/list" | jq

PowerShell equivalents:

$base = 'https://127.0.0.1:5000'

$ps = Invoke-RestMethod -Uri "$base/tasks/create/ps?seconds=10" -SkipCertificateCheck
$id = $ps.id

Invoke-RestMethod -Uri "$base/tasks/start?id=$id" -SkipCertificateCheck
Invoke-RestMethod -Uri "$base/tasks/state?id=$id" -SkipCertificateCheck
Invoke-RestMethod -Uri "$base/tasks/state?id=$id" -SkipCertificateCheck | Format-List
Invoke-RestMethod -Uri "$base/tasks/result?id=$id" -SkipCertificateCheck   # will be $null until terminal

Invoke-RestMethod -Uri "$base/tasks/list" -SkipCertificateCheck

Key Points

  • Multi-language (PowerShell, C#, VB.NET) with unified lifecycle.
  • Create and start are separate; second start returns false (idempotent).
  • $TaskProgress / TaskProgress provides live percent + status; completion sets 100.
  • /tasks/result returns only the result object (null until terminal state).
  • PowerShell result = pipeline objects; C#/VB result = last expression value.
  • Cancellation cooperates across languages (PS Stop(), C# token, VB awaited delay).
  • Removal only after task and any children are terminal (cascade uses snapshot to avoid collection mutation).

Troubleshooting

  • Task never leaves NotStarted: Ensure the runspace pool size is > 0 and the feature Add-KrTasks was invoked before Enable-KrConfiguration.
  • Cancel returns true but state stays Running: The script may be ignoring cancellation (e.g., tight CPU loop). Add brief sleeps or yield points, and emit progress updates via $TaskProgress.
  • Result is $null after completion: For PowerShell tasks that produce no pipeline output, this is expected. Emit objects or write output to capture a result array.
  • Removal returns false: Verify the task is in a terminal state and that any child tasks have also completed or stopped.

References


Previous / Next

Previous: None Next: Tasks Guide