Simon Vedder

Cloud Engineer · Zürich

Event-driven automation · Logic App

Deallocate on Activity Log

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.

licenseMITruns inLogic AppsdeployBicep · ARM · Terraformreacts inminutes, not hours

Activity Log · Resource Health · what the alert sees
# 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

One deployment, five resources

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

Deploy to Azure
Bicep or Terraform · resource-group scope
# 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.

One deployment creates

  • An activity-log alert rule on the whole subscription: category Resource Health, resource type virtual machine, cause UserInitiated
  • An action group that posts the common alert schema to the Logic App's HTTP trigger
  • The Logic App DeallocateStoppedVM with a system-assigned identity — four variables from the payload, one condition, one call
  • An API connection to the Azure VM connector, set up so the workflow authenticates as that identity rather than as whoever deployed it
  • One role assignment at the subscription, so the identity can deallocate a machine in any resource group

The 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

What it does

Reacts to one event

A guest shutdown, and nothing else. No schedule, no tags, no list of machines to keep current.

Reads Azure's own words

Resource Health describes a guest shutdown differently from an API stop, and the workflow checks for that sentence.

An API stop is left alone

az vm stop is deliberate. The condition is false and the run does nothing, which is the point.

One narrow role

The identity may read, start, power off and deallocate a VM. No write, no extensions, no run command, no login.

Three ways to deploy

Bicep, ARM behind the button, or Terraform. All three create the same five resources and all three were run.

Minutes

Azure raises the event about a minute after the shutdown and the alert pipeline adds one or two more.

Under the hood

One event, one sentence, one call

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.

  1. The guest shuts down

    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.

  2. Resource Health writes it to the Activity Log

    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.

  3. The alert rule calls the Logic App

    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

What was actually run

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.

  • Bicep, guest shutdown — Down at 11:52:16, deallocated by 11:55:09 — 2 min 53 s
  • Terraform, guest shutdown — Down at 12:31:50, deallocated by 12:35:04 — 3 min 14 s
  • ARM, the template behind the button, guest shutdown — Down at 12:44:24, deallocated by 13:00:25 — 16 min, with the alert pipeline delivering everything a quarter of an hour late that time
  • `az vm stop`, no deallocate — Health event titled Stopped by user, without the guest-activity sentence — left stopped, as intended
  • VM start, VM deallocate — Each triggers a run; the condition is false and nothing is done

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

Questions people ask first

Why not just tell people to deallocate instead of shutting down?

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.

How fast is 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.

Is this part of VM Power Management?

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.

What can the identity do?

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.

Why is the alert rule subscription-wide?

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.

How do I know it worked?

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 stranded machine is the cheap half

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.

Get in touch