Home / Azure

AzureHow-toIntermediate

Transformation: retire the 'scheduled task server' — move scripts to Azure Functions, Automation and Logic Apps

Move the cron jobs and Windows scheduled tasks that quietly run your operations onto managed Azure services. Inventory the scripts, pick the right service per job, run them with managed identity and add the monitoring the old server never had.

Deepak Bhardwaj3 min read

Applies toAzure Functions (timer trigger), Azure Automation runbooks with Hybrid Runbook Worker, Logic Apps, Container Apps jobs

The problem

Almost every estate has a VM that runs dozens of scheduled scripts: cleanups, reports, certificate checks, exports. It often runs under a personal account, isn't backed up, and nobody notices when a job fails.

1. Inventory

Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
  ForEach-Object {
    $info = $_ | Get-ScheduledTaskInfo
    [pscustomobject]@{
      Name = $_.TaskName; RunAs = $_.Principal.UserId
      Action = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join '; '
      LastRun = $info.LastRunTime; LastResult = $info.LastTaskResult
    }
  } | Export-Csv C:\temp\tasks.csv -NoTypeInformation

Linux: crontab -l for each user and /etc/cron.d/*. Record for each job: what it does, what it touches, run time, owner.

2. Choose a target per job

Job type Target
Short PowerShell/Python against Azure APIs Azure Automation runbook or Functions timer trigger
Needs on-prem access (file shares, AD, servers) Automation runbook on a Hybrid Runbook Worker
Integration: "when X, call API Y, send mail" Logic Apps
Long-running or custom dependencies (>10 min, big libraries) Container Apps job
SQL maintenance SQL Agent / elastic jobs, not a script server
No longer needed Delete it — usually 20–30% of the list

3. Example: timer-triggered Function

# function_app.py (Python v2 programming model)
import azure.functions as func, logging
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from datetime import datetime, timedelta, timezone

app = func.FunctionApp()

@app.timer_trigger(schedule="0 0 2 * * *", arg_name="timer")   # 02:00 UTC daily
def purge_old_exports(timer: func.TimerRequest) -> None:
    svc = BlobServiceClient("https://stexports.blob.core.windows.net", credential=DefaultAzureCredential())
    cutoff = datetime.now(timezone.utc) - timedelta(days=30)
    c = svc.get_container_client("exports")
    for b in c.list_blobs():
        if b.last_modified < cutoff:
            c.delete_blob(b.name)
            logging.info("deleted %s", b.name)

The schedule is NCRONTAB with six fields (seconds first). Use a managed identity with a narrow role, not a stored key.

4. Example: Automation runbook

# Runbook: uses the Automation account's managed identity
Connect-AzAccount -Identity
Get-AzVM -Status | Where-Object { $_.Tags['env'] -eq 'dev' -and $_.PowerState -eq 'VM running' } |
  Stop-AzVM -Force -NoWait
az automation schedule create -g rg-ops --automation-account-name aa-ops -n nightly-1900 \
  --frequency Day --interval 1 --start-time "2026-10-25T19:00:00+00:00" --time-zone "Europe/London"

5. Add what was missing

  • Alerting on failure: Functions → Application Insights alert on failed executions; Automation → alert on job status Failed via Azure Monitor.
  • Source control: scripts in Git, deployed by pipeline.
  • Secrets: Key Vault, accessed by managed identity.
  • Ownership: tag each job with an owner.

6. Cut over

Run the new job in parallel (or dry-run mode) for a cycle, compare outputs, disable the old task, and finally decommission the server.

Checklist

  • Every task inventoried with owner and purpose
  • Obsolete jobs deleted
  • Each job moved to the right service with managed identity
  • Failure alerts working
  • Old server retired