Reacts to one event
A guest shutdown, and nothing else. No schedule, no tags, no list of machines to keep current.
Event-driven automation · Logic App
Somebody shuts a machine down from inside Windows or Linux. Azure marks it stopped, keeps it on its host, and keeps billing it. This watches for exactly that event and deallocates the machine a few minutes later. One alert rule, one Logic App, no schedule and no tags.
# Shut down from inside the guest title Stopped by user or process cause UserInitiated details The Virtual Machine is stopping as requested by an authorized user or process, or due to a guest activity from within the Virtual Machine. deallocated, 2 min 53 s after the shutdown # az vm stop, from the API title Stopped by user details The Virtual Machine is stopping as requested by an authorized user or process. left alone
Deployment
Everything lands in one resource group, plus one role assignment at the subscription so the identity can reach any machine in it. The button deploys the ARM template; Bicep and Terraform create the same five resources. Give the alert rule five minutes before you test — a new activity-log rule is not live the moment it exists.
Needs Contributor on the resource group, and Owner or User Access Administrator on the subscription
# Bicep — into a resource group that exists
git clone https://github.com/simon-vedder/bicep && cd bicep/automations/deallocate-based-on-activitylog
az deployment group create --resource-group rg-automations --template-file main.bicep
# Terraform — same resources; the workflow body is fetched from the repository at apply time
git clone https://github.com/simon-vedder/terraform-azure && cd terraform-azure/automations/deallocate-based-on-activitylog
terraform init
terraform apply -var sub_id=<subscription id> -var rg_name=rg-automations
# Then: shut a test machine down from inside the OS, and watch its power state.UserInitiatedDeallocateStoppedVM with a system-assigned identity — four variables from the payload, one condition, one callThe Terraform variant needs the azapi provider for the API connection — azurerm cannot yet create one that authenticates with a managed identity — and loads the workflow definition from this repository's main branch when it applies. Pin the URL to a commit if that bothers you.
What it does
A guest shutdown, and nothing else. No schedule, no tags, no list of machines to keep current.
Resource Health describes a guest shutdown differently from an API stop, and the workflow checks for that sentence.
az vm stop is deliberate. The condition is false and the run does nothing, which is the point.
The identity may read, start, power off and deallocate a VM. No write, no extensions, no run command, no login.
Bicep, ARM behind the button, or Terraform. All three create the same five resources and all three were run.
Azure raises the event about a minute after the shutdown and the alert pipeline adds one or two more.
Under the hood
The whole workflow is four variables and one condition. What makes it work is that Azure writes down why a machine stopped, and says so in different words for a guest shutdown than for an API stop.
Windows or Linux powers off from inside. Azure sees the OS go away, sets the machine to PowerState/stopped and leaves it allocated on its host — the state Microsoft's own FinOps guidance says to avoid, and the meter runs on.
Within about a minute an event lands with category Resource Health, cause UserInitiated and the title Stopped by user or process. It arrives twice: an Activated entry with an empty description, then an Updated one carrying the sentence that matters — or due to a guest activity from within the Virtual Machine.
The rule matches category, resource type and cause, and the action group posts the alert to the workflow's HTTP trigger. The workflow takes machine, resource group and subscription from the payload and checks the description for the guest-activity sentence. A stop from the portal or the CLI reads as requested by an authorized user or process and nothing more, so it is left alone. A guest shutdown is deallocated through the Azure VM connector, as the managed identity.
Proof
Each variant deployed into a test subscription on 12 September 2026, against a Standard_B1s Ubuntu VM shut down from inside the guest with `shutdown -h`. Nothing here is inferred from the templates.
The runs found three defects in the templates as published in June 2025 before anything worked: the Bicep API connection was not set up for managed identity, the alert rule in Bicep and ARM filtered on a field the Activity Log does not have, and both called the workflow's management URL instead of the trigger's. All three are fixed in the repositories as of today; the Terraform variant had none of them. The identity's role was also narrowed from Virtual Machine Contributor to the power role above in Bicep and Terraform, which the ARM run had already proved sufficient.
FAQ
Because they will not, and the difference is invisible from inside the machine. The Start menu says Shut down, it works, and the bill keeps running. This makes the habit harmless instead of trying to change it.
Under three and a half minutes from the guest going down to deallocated, in two of three runs. The third took sixteen, because Azure's alert pipeline delivered every event a quarter of an hour late that afternoon — nothing in the workflow changed. Plan for minutes, not seconds, and not for a guarantee.
No. That is an hourly controller with schedules, guards and a record of every decision; this is a reflex. They coexist: the controller also deallocates a stopped machine, on its next hourly run. If the stranded machine is your only problem, this is enough and it is faster.
Read, start, power off and deallocate virtual machines, anywhere in the subscription. It cannot change a machine, install an extension, run a command inside one or log in. The templates used to hand it Virtual Machine Contributor, which can do all of those; they no longer do.
Because the machine that gets shut down is not known in advance. The workflow takes the machine's own subscription and resource group from the event, so narrowing the rule's scope to a resource group in the template is the only change you need for a smaller radius.
The Logic App's run history shows every trigger and which branch it took; the machine's power state goes from stopped to deallocated. Test it once: deploy, wait five minutes for the rule, shut a test machine down from inside, and watch.
The expensive half is the machine nobody can name the purpose of, running around the clock because switching it off feels riskier than paying for it. I run that review as the first step and stay for the deployment.