Automation of gathering and importing Windows Autopilot information

Complete process automation of gathering and upload of a device Autopilot information to the Windows Autopilot service with an Azure Automation Runbook.

On one of my previous blog post Gather Windows 10 Autopilot info in azure blob storage during wipe and reload, I described the gathering of Autopilot information during operating system deployment in a wipe and reload scenario with MDT. Just a short recap of the problem and my initial solution:

If we purchase a new device, the OEM vendor takes care of installing Windows 10 with a signature edition or provisioning ready installation including all necessary drivers. If we buy new hardware the information for Autopilot can be synced into our tenant from the OEM vendor (Lenovo is already capable of doing that and others will follow). We will get the device information in Intune and we can start to assign an Autopilot Deployment Profile and start to enroll the devices.

What if we have a bunch of Windows 7 devices in the environment?

A way to handle this is that we are playing the role of the OEM vendor and do the install of a Windows 10 signature edition on the existing Windows 7 devices, gathering Autopilot information, and let Windows 10 start in the Out of Box Experience (OOBE) again for user enrollment. Depending what is available we can use ConfigMgr or MDT for this. My example uses MDT.

Now imagine a situation where a rollout team is preparing a lot of machines. We would end up in a lot of .csv files on different devices. To make this a little easier for IT to import the hardware information of new devices into the Autopilot service, we build up the following logic:

  1. Gather hardware information via PowerShell Script Get-WindowsAutoPilotInfo during wipe and reload
  2. Upload .csv file via AzCopy to an Azure Blob Storage
  3. Gather .csv files from Azure Blob Storage and combine them into a single combined.csv file
    This was a manual step in my previous solution
  4. Upload combined .csv file to Autopilot and assign Deployment Profiles
    This was a manual step in my previous solution
  5. Device can be delivered to the end user like it where shipped by the OEM vendor

You can read more about the initial solution here: Gather Windows 10 Autopilot info in azure blob storage during wipe and reload

This blog post is all about automating these two steps – gathering and upload of Autopilot information to Intune.


First, I will explain the architecture and how it works and then I’m going to describe the way to implement it. The overall architecture is based on an Azure Automation Runbook and looks like this:


The new procedure including the enhanced logic for a complete automation of the import is now as follows (modified steps for complete automation):

  1. Gather hardware information via PowerShell Script Get-WindowsAutoPilotInfo during wipe and reload scenario
  2. Upload .csv file via AzCopy to an Azure Blob Storage
  3. Gather .csv files from Azure Blob Storage and combine them into a single .csv file with the help of a scheduled Azure Runbook
  4. Upload combined .csv file information to Windows Autopilot Service via PowerShell Script WindowsAutoPilotIntune running in an Azure Automation Runbook 
  5. Cleanup Azure Blob Storage (delete all .csv files of successfully imported devices and delete all .csv files of already imported devices)
  6. Generate import notification and summary and post it to a Microsoft Teams channel
  7. Autopilot information is available for the OOBE user enrollment scenario with Autopilot. The Autopilot profile gets automatically assigned by a dynamic AzureAD device group membership.
  8. Device can be delivered to the end user like it where shipped by the OEM vendor

I’ve still chosen the copy via AzCopy of individual .csv files to the Azure Blob Storage approach as we can then limit the access quite well via shared access signature and we can easily limit permission to write blob objects only. No need to provide credentials or Blob Storage keys on the client side. Sure, we could build up a more advanced HTTP endpoint to gather device information, but this approach is quick and simple. I’m pretty sure that the complete solution of this automation task is something which we do not need in future when devices are Windows 10 migrated and we then buy Autopilot ready hardware only.


Guide to build the new solution

The Autopilot Graph API is an API with focus on batch processing. This means we import new device information into a kind of staging area and Windows Autopilot service will pick up the new device information and starts importing it. This process varies in the amount of time it takes and we have to check the status of all devices to get the import result. As soon as the devices are processed we can clean up the staging area and the import is done. Normally we would do this by wrapping the Graph API calls (REST) into some PowerShell functions and build the logic for the described process. Luckily Microsoft released a new PowerShell Module WindowsAutoPilotIntune (thx to @mniehaus) based on the Graph API to import new AutoPilot information into Intune.

In my previous blog post about Process automation for Intune and Azure AD with Azure Automation, I created a Runbook to delete old devices from Intune via Graph API and demonstrated how to do an unattended authentication within the Runbook. All the details how this can be achieved are explained there. Please follow the guide to setup the Azure Automation account. I use the same unattended authentication technique to utilize the PowerShell Module WindowsAutoPilotIntune to import the device information into Autopilot service in the following Runbook. Additionally, the Runbook is built to protect concurrent execution (thx to Tao Yang, I used his implementation for it) to ensure a sequential processing and to keep track of current running imports. If we would design this as a concurrent solution it would get much harder in terms of monitoring and reporting in the end. In addition, there is a max import of 175 devices into the staging area of the API which we are taking care of by limiting the Runbook import to 175 devices during one run.

If the requirements are implemented based on the previous blog post (especially the Intune native app and the automation service account) we simply need to extend the permissions to “Read and write Microsoft Intune configuration” of the automation account which was created during the setup of Process automation for Intune and Azure AD with Azure Automation in section Building the solution.


For the concurrent execution protection, we need our automation credential to have Reader permission and for Blob Storage access we need Contributor permissions on the subscription. As the result we grant Contributor permission to the automation account:


Finally, we can implement the complete Runbook which can be found on my GitHub account here:

The Runbook is written in PowerShell and follows the logic described in the beginning of this post – section architecture.

Create a PowerShell Runbook and paste-in the code.



To make sure the Runbook successfully runs we need to define some additional variables. I assume that the IntuneClientId and Tenant variable are defined as described in the previous blog post.


Additional variables needed for Azure Blob Storage access:

ContainerName: <your-blob-storage-containername>
StorageKey: <your-blob-storage-secret-key> * as encrypted variable

Additional variables needed for Microsoft Teams Channel notification:

SubscriptionUrl: <your-subscription-url>

The subscription URL can be found as shown below. Please do not copy the /overview end of the URL. The URL should end with the subscription GUID only (like highlighted):


TeamsWebHookUrl: <your-ms-teams-webhook-url>

Open MS Teams and choose a Channel where the Autopilot notification from the Azure Runbook should be displayed. Click on the three dots and choose Connectors:


Look for Incoming Webhook and click Configure


Type in the details and upload a icon and click Create


Finally copy the Webhook URL:


Paste it into the Azure Automation variable TeamsWebHookUrl and set encrypted value to Yes


This is necessary to get Microsoft Teams notifications with some statistics and information to troubleshoot errors. Below is an example of an import notification in Microsoft Teams:


We have some statistics, detailed error list with device information and a link to the Runbook itself in Azure. All based on Adaptive Cards JSON code. This can be easily modified to fulfill personal needs. Have a look at the Adaptive Cards Designer ( for experimenting with layouts and adjust the Runbook code.


Enhanced client-side script part

I my previous blog post about Gather Windows 10 Autopilot info in azure blob storage during wipe and reload I have described how to setup the Azure Blob Storage to gather the “<hostname>.csv” files during MDT operating system installation. Please follow the previous guide to setup the Azure Blob Storage and the integration in MDT.

I have an enhanced version of the gather script now which can be found on my GitHub account and is also shown below. The enhanced version does not have the dependency on AzCopy.exe (incl. dependency files) and Get-WindowsAutoPilotInfo.ps1 in the script directory. If they are not available, they are downloaded from an additional Blob Storage container named resources. The additional container resources must be created and the and Get-WindowsAutoPilotInfo.ps1 must be uploaded there to successfully run the script:



The enhanced Get-WindowsAutoPilotInfoAndUpload.ps1 version:

Replace ZZZZ with your Blob Storage account name and ZZZZ with your SAS signature in the script above. See here Delegating Access with a Shared Access Signature for more SAS signature details.

This version can also be used to be executed via Microsoft Intune Management Extension to run it on existing Windows 10 devices. It is possible to collect all targeted device information and the Runbook will import the device information. Already imported devices will be skipped. This way we can make sure every device is imported to the Autopilot service.



Sample output of the Runbook


If a device information <hostname>.csv is successfully imported the .csv files will be deleted from the Azure Blob Storage. In case of an error it will be left there untouched but reported via Runbook output and Teams notification. There is one case where the Runbook will delete the .csv file from the Azure Blob Storage also. This is if the Runbook detects an error 806 – ZtdDeviceAlreadyAssigned. In that case we can delete the .csv as it has no consequences. In every other error situation someone needs to resolve the error manually. The Teams notification is only generated if there is some device information in the Azure Blob Storage. The normal procedure would be if operations gets an import notification it should check and in case of errors they should be resolved manually.



Important observations during testing

The Autopilot import via Graph API takes some time and it may also timeout. So, it is not a typical request response REST API in this case. Remember all device information is staged and then the devices are monitored for their success or error state. This led to situations where I had to wait up to 1 hour to get a successful import, even when the UI in Intune tells us it takes up to 15 min. So be prepared that it might take longer or fails during the run. I chose this approach as it is the same procedure as the Azure Portal does the import. In fact, we really automated the import process in Intune but did not invent a complete different process which might cause different problems of device tracking or concurrency and so on. Depending on the use case you can run the Runbook on a recurring schedule. My normal use case for this scenario is to support people who are preparing older devices when they are getting reinstalled from Windows 7 to Windows 10. If someone expects immediate availability of Autopilot information after such a reinstall this might be problematic as the API is not designed for this. Another fact is that the Autopilot Deployment Profile assignment using Intune does take some time also at the moment. I observed that it took several hours sometimes. I suggest to re-arrange the operational processes and hand out reinstalled Windows 10 devices after some additional time and not directly after reinstalling, as this increases the possibility that the Autopilot information is not imported and profile assigned yet.

To run the Runbook on a recurring schedule just go to the Runbook and add a schedule. The max recurrence is limited to every hour.


Just define a schedule (max once per hour) and monitor the recent job overview if it works:


The schedule can even be enhanced by using a simple Logic App and not using the Azure Automation Runbook schedule at all:


Please see Stefan Strangers post for detailed instructions how to implement a simple Logic App to trigger an Azure Automation Runbook:

In case something goes wrong I have created a second Runbook to clean up the staging area of the Autopilot Graph API. Get it from my GitHub account and run it in case of fatal errors where you might want to clean up the staging area:

Here is a sample output of the Cleanup Runbook:


Further information

Azure Logic Apps – Schedule your Runbooks more often than every hour

Preventing Azure Automation Concurrent Jobs In the Runbook

Post notifications to Microsoft Teams using PowerShell

importedWindowsAutopilotDeviceIdentity resource type

Autopilot profile assignment using Intune

Adaptive Cards Designer

I published the same article on SCConfigMgr in a more step-by-step guide version, meaning there are not so many cross references to my other articles:

Automation of gathering and importing Windows Autopilot information


I hope this can increase your throughput on the way to an Autopilot Windows 10 modern management environment.

When someone finds bugs or problems with the solution let me know and leave a comment. I will do my best to fix them as it should be a reliable part during preparing old devices with Windows 10.

Intune Managed Browser (MAM) with Azure AD Application Proxy and Conditional Access

Recently Microsoft enhanced the Intune Managed Browser experience with Mobile Application Management (MAM) and app-based Conditional Access (CA) a lot. It is integrated into the Conditional Access story as an approved app and supports the Azure AD Application Proxy very well now.


What does this allow us to do now?

We are now able to design a solution to publish our internal websites externally with minimal effort and then allow access to it from our mobile devices only by the Intune Managed Browser protected by Intune app protection policy. This ensures the information is safeguarded in our containerized Intune MAM solution. This gives most companies enough trust to actually do the publishing of internal resources for usage on mobile devices and support the bring your own device (BYOD) solution.

Please read the How does Application Proxy work? documentation from Microsoft to get a better understanding what we are going to do in the next section with the Azure AD Application Proxy. The Azure AD Application Proxy architecture is shown in the figure below:


One of the nice things is it will not require us to open up any inbound firewall ports. As long as we are allowed to make outbound connections we can publish internal websites easily to external. The solution even supports various authentication scenarios inclusive Single Sign-On (SSO).


Here is a walkthrough of a demo setup to show it in action

The walkthrough of the demo scenario should get you a deeper understanding of the new possibility. Assuming we have some internal websites e.g. intranet and expenses and they are available in the internal network only. To simulate that, I have setup an IIS server hosting the two simple websites, intranet and expenses within a private network. They are reachable on the IIS server via http://localhost for intranet and http://localhost:81 for expenses. In addition I have a link from intranet pointing to expenses website (link target is: http://localhost:81, compare screenshot with html source code). I built the two demo sites to also demonstrate link translation with Azure AD Application Proxy later on.




How do we get the internal websites published now?

First of all we need to switch off the IE Enhanced Security Configuration on the Windows Server otherwise we are not able to complete the login prompt of the Azure AD Application Proxy during setup procedure. Then we are downloading the Azure AD Application Proxy on our demo IIS server and run the msi installer. It’s a very lightweight installer and the only thing we need to provide is the Global Administrator credential during setup to finish the process.


The next step after installing the connector is to enable it by clicking Enable application proxy. After it is enabled the UI switches to “Disable application proxy” (shown in screenshot as step 3). Once enabled we have the Connector group default and our server listed there. It is possible to install more then one connector and build connector groups to support better reliability of the publishing (in fact this is recommended). The connector does not need to be installed on the IIS as I have done it in my demo setup, it should be on a dedicated Windows Server 2016 for example. I needed to run it on the IIS for simplicity of my setup and to use the internal address of http://localhost during publishing later on.

The official documentation for the Azure AD Application Proxy from Microsoft is found here or you follow the link on the application proxy blade “Learn more about Application Proxy“.

With an up and running connector we can publish the websites now. It is the best to follow the detailed step-by-step guide from Microsoft and make both available. I published my both sites as an Enterprise Application as described and used no custom domain, but enabled link translation in the application body.

Published internal websites:


Details of the website intranet with internal URL http://localhost


Details of the website expenses with internal URL http://localhost:81


Now I can open up my published intranet from external and the intranet link originally pointing to http://localhost:81 was replaced by the application proxy because we enabled link translation on the application body (compare screenshot below). This works only if we publish both websites as the application proxy must find a published website for http://localhost:81 to do the translation.


In a real world implementation I would recommend to use a custom domain for publishing to maintain your links. For example if we have as Active Directory (AD) and I publish via Azure AD Application Proxy with the custom domain I can reach the website internally and externally with the same URL. To set this up follow the instructions here:

Working with custom domains in Azure AD Application Proxy


Securing our Intune mobile apps with Intune application protection policies

Now we need to add a MAM policy – app protection policy to secure the Intune Managed Browser and Mobile Outlook. To do that we open Intune > Mobile apps > App protection policies > Add a policy


After adding the policy we make sure Outlook and the Managed Browser is in the targeted apps and of course we adjust the individual Policy setting to meet our corporate standard and to realize the containerization (e.g. let apps only transfer data to other managed apps, encrypt data and so on…).


For the policy setting we need to make sure the setting Restrict web content to display in the Managed Browser is set to Yes. This makes sure internal links in emails are opened in the Intune Managed Browser. Even better because of the Azure AD Application Proxy publishing we make sure that internal links get translated and opened successful in Intune Managed Browser. We will do that by assigning an additional app configuration policy in the next step.


As last configuration we assign the app protection policy to our AAD user group we want to target.

To configure the Intune Managed Browser to work hand in hand with the Azure AD Application Proxy and translate internal URLs to the published URLs we need to configure an app configuration policy for the managed browser.



Now the important piece of configuration is to configure:

Value: true

The screenshot below does not display the complete string!


Again as last configuration we assign the app configuration policy to our AAD user group we want to target.


Controlling access to the internal websites with app-based Conditional Access

Now we need to make sure our internal published website can only be accessed by Intune approved apps which are protected by app protection policy.

To do that we create the following Conditional Access policy in Intune or in the Azure AD portal. We assign our AAD user group, target All cloud apps, and include iOS and Android devices, and select Browser and Mobile apps desktop clients


As access control we grant access for approved client apps by choosing the option Require approved client app



How about the user experience?

Everything is in place and we assume someone in the company sent us an internal link to the new intranet site http://localhost. We open up mobile Outlook on iOS in this example:


If we now click on the internal link, Outlook is configured to Restrict web content to display in the Managed Browser and will open the link in the Intune Managed Browser for us. The Intune Managed Browser is then instructed for AppProxyRedirection = true. This will redirect us to the external published URL instead of the internal URL as shown below and shows us the demo intranet site:


Even the link within the demo intranet site is translated and will open the published demo expenses website:


To make sure that the published intranet site is only accessible by the Intune Managed Browser we open up Safari and open the published intranet site by typing in the external URL and we will check if access if blocked:


As we see the access is blocked and we get a nice feedback to use the Intune Managed Browser instead and we can directly use the blue link button to open the Intune Managed Browser.



We have seen how to publish internal websites via Azure AD Application Proxy easily. Then we configured our mobile apps to use an Intune app protection policy and instructed the Intune Managed Browser to use Azure AD proxy redirection to translate internal links and open them successfully. We achieve protection of the published internal website to prevent data leakage.


Further information

The Intune Managed Browser now supports Azure AD SSO and Conditional Access!

Better together: Intune and Azure Active Directory team up to improve user access

Manage Internet access using Managed Browser policies with Microsoft Intune

How to create and assign app protection policies


My advice to all, give it a try and start to play with MAM and app-based Conditional Access as it might be a quick win for your company and finally allow the usage of BYOD as company data can be protected very well in this scenario.

Happy publishing and protecting 🙂

Process automation for Intune and Azure AD with Azure Automation

IntuneAndAzureAutomationCloud managed environments benefit from the idea of software as a service, you don’t have to think about upgrading or maintenance of the infrastructure itself. But often we need to automate the tools itself. A very good example here is when an employee quits his job, than we need to trigger a lot of processes like disabling the account, retire of the device(s), wiping of the devices, sending some notes to various people and so on. Another example might be the cleanup of devices within Intune and Azure AD as they get stale over time and they are not used by users anymore.



In the following blog post I like to show how to automate the process to delete old devices from Intune and Azure AD without the help of services from on-premises like servers running scheduled scripts. The established cloud workflow can be used by the service desk to quickly delete a device in both involved services Intune and AAD. After seeing a lot of environments where devices are being cleaned up in Intune and left in AAD, I thought its beneficial to show how to easily automate this with the Microsoft cloud solution Azure Automation. If the basics are built it’s just a matter of combining new tasks within a Runbook to build other workflows which are worthwhile in your environment.

I will show how to setup the Azure environment and create the first Runbook. A Runbook is the actual workflow which runs the PowerShell script. The Runbook will do an unattended authentication against the Inunte API via Microsoft Graph to manage Intune. We do not have a PowerShell module for Intune at the time of writing therefore we use the Intune API in Microsoft Graph. For the AAD operations we use the AzureAD module to perform the management tasks.


How to do unattended authentication with the Intune API?

The problem with the Intune API and Microsoft Graph is, that we can’t authenticate as an application as this is not supported at the time of writing. See section here Intune Device Management permissions > Application permissions: None.

We need to authenticate as an user (service account). This requires additional credentials and a secure storage of them to automate. Microsoft has a good guide how to set up an Azure application to support this scenario: How to use Azure AD to access the Intune APIs in Microsoft Graph. One aspect is that the Microsoft How-To guide will end up in a scenario which still prompts for credentials with a input form. This is because of the usage of:


For Azure Automation we need to change this behavior a bit to support credentials within our code:


We can use the How-To guide or the official GitHub Intune sample scripts which have the following lines of code:

$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
$platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"
$userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")
$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters, $userId).Result

they need to be changed to support our new AcquireTokenAsync call with support to specify UserPasswordCredentials as additional parameter:

$intuneAutomationCredential = Get-AutomationPSCredential -Name automation

$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
$platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"
$userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($intuneAutomationCredential.Username, "OptionalDisplayableId")
$userCredentials = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential -ArgumentList $intuneAutomationCredential.Username, $intuneAutomationCredential.Password
$authResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceAppIdURI, $intuneAutomationAppId, $userCredentials);

The credentials will be received from the Azure Automation account in PowerShell via Get-AutomationPSCredential. We will provision the service account credentials securely for the Azure Automation account via Credential assets.


Building the solution

The following steps must be followed to build the solution:

  1. Creation of a native application in Azure AD
  2. Assigning permissions to the registered application
  3. Grant permissions (administrator consent)
  4. Create Azure Automation Account
  5. Add Azure AD module to the Azure Automation Account
  6. Add credentials to the Azure Automation account
  7. Add variables to the Azure Automation account
  8. Add Runbook to the Azure Automation account
  9. Edit Runbook
  10. Start and test Runbook
  11. Add Automation Operator


1. Creation of a native application in Azure AD

The best description for a native application is found in the Intune documentation for the Intune API here: How to use Azure AD to access the Intune APIs in Microsoft Graph. I will outline the necessary steps to setup the environment.

New application registration


fill out the details and give it a name, create a native application with redirect URI: urn:ietf:wg:oauth:2.0:oob


in the end a new registered application is available. Important is to copy the application id as we need it in our PowerShell script Runbook later.



2. Assigning permissions to the registered application

The registered application must have AAD Read and Write permissions, and Intune Read and Write permissions.






3. Grant permissions (administrator consent)

Finally we grant the selected permissions to the newly registered application.




4. Create Azure Automation Account

Creation of the Azure Automation Account in a existing or new resource group.




5. Add Azure AD module to the Azure Automation Account

To have access to AzuerAD module we add it via the Gallery, choose Browse Gallery




6. Add credentials to the Azure Automation account

Go to Azure AD and create a new user, in my case user automation with Display Name Intune Automation and use a complex password for it.


At the moment we need to assign the Global Administrator role as we want to delete devices in Azure AD. This information is based on:

After user creation we add the credential to the Azure Automation account.




7. Add variables to the Azure Automation account

The following PowerShell needs the native registered application ID also called Client ID. Therefore we create a Azure Automation variable IntuneClientId and we need the tenant ID as a variable, we use Tenant as identifier.


Below an example for IntuneClientId and add your Application ID from above, do the same for Tenant variable and add your tenant ID.



8. Add Runbook to the Azure Automation account

Adding a Runbook with the name Invoke-RetireDevice




9. Edit Runbook

We verify if the Runbook sees all our important information like AzureAD module, Variables and Credentials. After adding the PowerShell script we need to publish it.


PowerShell script for the Runbook is based on the GitHub samples with the modification to allow non-interactive usage of credentials via Get-AutomationPSCredential and Get-AutomationVariable

$intuneAutomationCredential = Get-AutomationPSCredential -Name automation
$intuneAutomationAppId = Get-AutomationVariable -Name IntuneClientId
$tenant = Get-AutomationVariable -Name Tenant

Now follows the actual PowerShell script with the logic to get the device of the user and delete it form Intune with usage of the automation credentials and variables for client id and tenant. In the end it will use the same credentials to delete the device from AAD also.


10. Start and test Runbook

Everything is setup, now it’s time for the first run. Get a stale Intune device you like to retire and start the Runbook.


The Runbook has two input parameters DeviceName and UserPrincipalName. This is needed to avoid getting duplicate entries for DeviceName. A user should only have a device once. If not we might rethink the PowerShell logic to address this.


After start of the job we can click on Output


and get details as defined in our PowerShell script. If everything runs fine you will get the following output:



11. Add Automation Operator

We add a different user (e.g. service desk operator) to our Runbook as an Automation Operator. This provides the user the possibility to log on to and start the Runbook but it’s protected from modifications as shown below.





Recap what we achieved

We have setup Azure Automation to host our PowerShell script in a managed cloud environment which is able to run as job to delete an Intune device and AAD device. In addition we learned the basics of Azure Automation and how to add modules, work with credentials and variables. Usage of unattended authentication to the Intune API is the basis for Intune API usage in Azure Automation.



  1. Microsoft Flow
  2. Source Control

Microsoft Flow

I thought it would be nice to enhance the Runbook with a Microsoft Flow to trigger it from my mobile phone. I found the following article which is describing how to do that:

Azure Automation new Microsoft Flow Service

Unfortunately as soon as I tried to use it I found that Microsoft Flow does not provide any trigger at the moment for it. The manual trigger as shown in the blog post above is not available for me. Maybe we can provide a nice interface for the runbook in the future via Microsoft Flow.

Microsoft Flow – Azure Automation

Source Control

When working with code it’s important to have a good versioning and a code storage place. For this Azure Automation provides integration with GitHub to link your Runbook source code. How to setup this follow the guide below. I can really recommend it.

Source control integration in Azure Automation


Further information

Azure Automation User Documentation

How to use Azure AD to access the Intune APIs in Microsoft Graph

Credential assets in Azure Automation

Intune Device Management permissions

Graph Explorer – Microsoft Graph

Another very good guide using Azure Automation with Intune and AAD is here:
Unattended authentication against the Microsoft Graph API from PowerShell,graph,api/2017/10/30/unattended-ms-graph-api-authentication.html

You want to learn more about Intune Housekeeping with scheduled Azure Automation PowerShell scripts then visit Ronny’s blog:


Have fun in automation. Feel free to post your process automation ideas in the comment area below! Thanks for reading!

Use Delivery Optimization with DHCP Option on Pre-Windows 10 version 1803

The new Windows 10 Peer 2 Peer feature Delivery Optimization was enhanced by the setting to query DHCP option ID 234 to get a Group ID (DOGroupIdSource). It was implemented into the latest Windows 10 version 1803 based on my feedback. I’m a little proud that the idea was well received and my product feedback was implemented as new option within six month.

It was officially announced with Windows Insider Preview 17063. It can be read here: in the section New features for IT Pros in Delivery Optimization.

I’ve written a blog post how Delivery Optimization works and how to use this feature with new Windows versions 1803+, see here: Configure Delivery Optimization with Intune for Windows Update for Business.  It’s really great to have it with Windows 10 version 1803+ but I need to provide this functionality to older Windows 10 versions (1703 and 1709) also. I want to use this very good and flexible grouping capability from now on for all devices even pre-version 1803.

Therefore I have designed a solution to provide this functionality to older versions of Windows 10 and switch over to the native Windows 10 implemented solution as soon as the device gets upgraded to 1803.

First problem is to get the DHCP option from the DHCP server. Luckily I’ve written a small C++ program to do that in the past. I’ve written it to even work with a WinPE environment. The small binary can be used to send the DHCP option ID x request. When executed the binary expects a result as a string value. Below an example:

DhcpOption.exe <OptionID>


It’s even possible to specify a debug switch

DhcpOption.exe <OptionID> [debug]

to get debug output for troubleshooting:


The C++ solution can be found on my GitHub here:

The compiled x64 binary DhcpOption.exe can be found here:

With this little helper we can use the Intune Management Extension and design a PowerShell script to:

  1. query DHCP server for the Option ID 234
  2. write result (Group ID) to the registry

Prerequisite is an available Option ID 234 on the DHCP server. See Configure Delivery Optimization with Intune for Windows Update for Business how to configure the DHCP server for this.

To support travelling users we need to make sure the device will query the DHCP server from time to time to get the group ID belonging to the current DHCP scope the client is using. For this we schedule the script as a scheduled task and run it at logon and on every unlock of the workstation.

The Delivery Optimization service will query the registry value for every new request and this makes sure a client uses the Group ID delivered by DHCP from the particular site.

To make sure we do not interfere with the native implementation starting with 1803 I implemented a logic to disable the scheduled task and remove the registry key as soon as Windows 10 version 1803, a build greater than 16299 is found.

The DhcpOption.exe will be provided from an Azure Blob Storage account. We need to create a Storage account as type Blob storage and a container to store the DhcpOption.exe. Then we use the provided download link in the script to get the binary during install. A guide how to create the Azure storage account is included in my article Gather Windows 10 AutoPilot info in Azure Blob Storage during wipe and reload. You don’t need the SAS token, just the container and upload DhcpOption.exe. The SAS token is only needed to support POSTS methods for uploading new files to the blob storage. The DhcpOption.exe is uploaded once and then provided for download only.

With this solution we can use the DHCP option for all Windows 10 version until 1803 and as soon as we get upgraded we disable and remove our self and the native implementation takes over. To make sure the native implementation is used as soon 1803 is found we need to implement the new DO settings (DOGroupIdSource). Follow my guide in my previous post for this Configure Delivery Optimization with Intune for Windows Update for Business.

Source Code of the full custom DO install script Register-DOScript.ps1 can be found on my GitHub Gist here:

Make sure to replace the Azure blob storage URL with your own one!

The uninstall script Unregister-DOScript.ps1 is also available via my GitHub Gist:

If we combine the script with Intune Management Extension we can easily deploy this solution to our Azure AD joined modern managed Windows 10 pre-version 1803 devices.

We will get the files for scheduling at C:\ProgramData\CustomScripts


and the scheduled task RunCustomDOscript


with task trigger logon or unlock of workstation


when executed we will get the Group ID from the DHCP server

MD DHCP Group ID Option in Scope

and the received string is written to the registry


for me this bridges the time until all devices are upgraded to Windows 10 version 1803 and gives me enough flexibility to group my devices to achieve maximum Delivery Optimization benefits.

Happy caching!

Part 2, Deep dive Microsoft Intune Management Extension – PowerShell Scripts

Because of the popularity of my first blog post Deep dive Microsoft Intune Management Extension – PowerShell Scripts, I’ve decided to write a second post regarding Intune Management Extension to further explain some architecture behind this feature and upcoming question from the community. A deeper understanding helps to successful troubleshoot the feature.

Table of content for easy navigation


Details of the MSI deployment of the Intune Management Extension agent?

Please read the first article Deep dive Microsoft Intune Management Extension – PowerShell Scripts, to get an understanding of the MSI install job.

The EnterpriseDesktopAppManagement CSP takes care of this task and delivers the MSI to the device and starts the execution. The CSP provides some helpful information in registry for troubleshooting.

In the registry at HKLM\SOFTWARE\Microsoft\EnterpriseDesktopAppManagement\<SID>\<MSI-ProductCode> you can find helpful information for troubleshooting:


Highlighted value names are described in detail here:

CurrentDownloadUrl: URL to the MSI install file.

EnforcementRetryCount: The number of times the download and installation operation will be retried before the installation will be marked as failed.

EnforcementRetryIndex: The current number of retry.

EnforcementRetryInterval: Amount of time, in minutes between retry operations.

EnforcementStartTime: Start time of enforcement.

EnforcementTimeout: Amount of time, in minutes that the installation process can run before the installer considers the installation may have failed and no longer monitors the installation operation.

LastError: Error after last execution.

Status: The Status can have the following values according to the EnterpriseDesktopAppManagement CSP documentation:

Value = Status
10 = Initialized
20 = Download In Progress
25 = Pending Download Retry
30 = Download Failed
40 = Download Completed
48 = Pending User Session
50 = Enforcement In Progress
55= Pending Enforcement Retry
60 = Enforcement Failed
70 = Enforcement Completed

In case of no sidecar agent on the device the status may indicate an error or it is still in progress of downloading. Try to figure out this status. Correlate the status with your device and environment. There may be a proxy issue?

According to the screenshot above the CSP will try it 3 times with a timeout of 10 min and then it will be marked as failed. Is there a retry after x days, I assume yes but I don’t have some documentation for it. As soon as I have more information I will update the article accordingly.


Be aware of the log files regarding sensitive data!

The following script below will create a local user account with a specified static password. It is a working script, but be aware that it should not be used in production! The whole script content will be logged to the IntuneManagementExtension.log file. There you will find the clear text static password as defined in the script. As an example I have copied and marked the particular log file entries below the script to show that. Disk encryption technologies like BitLocker are lowering the risks a bit, but even then this is not a concept to be used in production environments! An attacker can read the log files with standard user permissions and get the sensitive data. Without BitLocker encryption this can be done even offline!

Again: I do NOT recommend transferring sensitive data like passwords via this approach! I know it’s often done. There is a similar approach for on-prem AD which is often used. The GPO preferences for local user management tasks uses a simple xml file with passwords base64 encoded -> this is not secure! Please consider something that was build for that purpose. Admin account management should be done via LAPS or other third party. I know there is no equivalent solution like LAPS for a cloud only environment right now from Microsoft.

# Author: Oliver Kieselbach
# Date: 01/03/2018
# Description: Create a local user account.

# REMARK: DO NOT USE IN PRODUCTION, Password will not be unique and be visible in clear text in log file!

# The script is provided "AS IS" with no warranties.

Param([switch]$Is64Bit = $false)

Function Restart-As64BitProcess
If ([System.Environment]::Is64BitProcess) { return }
$Invocation = $($MyInvocation.PSCommandPath)
if ($Invocation -eq $null) { return }
$sysNativePath = $psHome.ToLower().Replace("syswow64", "sysnative")
Start-Process "$sysNativePath\powershell.exe" -ArgumentList "-ex bypass -file `"$Invocation`" -Is64Bit" -WindowStyle Hidden -Wait

if (!$Is64Bit) { Restart-As64BitProcess }
Start-Transcript -Path "$env:temp\CreateLocalUser.log"

$password = ConvertTo-SecureString "SecretP@ssw0rd!" -AsPlainText -Force
New-LocalUser "John" -AccountNeverExpires:$true -FullName "John Doe" -Password $password


To demonstrate the problem with this approach, here is the corresponding log file part after script execution with the complete script and static password in clear text:



Getting content of scripts once they are uploaded to Intune?

The Intune Azure Portal does not provide any UI element to show the uploaded script again. Thanks to the GitHub repository Intune PowerShell Samples we do not script something by our own. We can use a script provided from there called DeviceManagementScripts_Get.ps1. It will get all uploaded scripts, including script content, and details from the Intune API via Microsoft Graph API.

Get it from here:

Save it locally and run it with PowerShell and provide your Intune Administrator credentials when asked for:



What about return codes (exit codes) of PowerShell scripts?

At the moment the Intune Management Extension will gather various results, but the Intune Azure portal does not show them in an UI element (if it will change in the future and we have something available, I will update the post accordingly). As for now scripts can be executed through the Intune agent and the Intune UI will show just the execution state success or failure. The status is related to successful agent execution, like no hash mismatch, no script content problem, and no error output… it does not reflect the script exit code.

Scripts like this, I called it “failscript.ps1” are handled as success at the moment:


See here the results in Intune Azure Portal:


Perhaps Intune will show us script exit codes in the UI sometime, then we could verify this easily with the failscript from above.

If we use Write-Error cmdlet in our scripts then Intune will pick up the error output. Here an example:

Write-Error -Message "Could not write regsitry value" -Category OperationStopped

This gives us a failed status in the monitoring section of the PowerShell scripts:




Where can I find a helpful PowerShell script template to start with?

I have made a simple script template which:

  1. Enforces execution in x64 PowerShell (restart of PowerShell as x64 process)
  2. Generates a log file in C:\Windows\temp when run in system context
    or %LocalAppData%\temp when run in user context
  3. Has exit code handling (exit code is even gathered when restarted as x64 process)
  4. Uses Write-Error to signal Intune a failed execution and makes sure we have reporting reflecting our script execution success (standard error is even gathered when restarted as x64 process)

For ongoing maintenance it is provided via GitHub Gist:


Script execution retry interval when failed?

The PowerShell scripts are executed via agent on the target device. If an execution fails, the agent tries to run the script again during next check-in. Current check-in interval is every 60 minutes. This procedure is limited to 3 attempts!

This is tracked in registry at:


The DownloadCount also means execution count and the result is tracked as Success or Failed.


If we change the run as account (user/system), signature check or script content, the DownloadCount will be reset, and the agent will try another 3 attempts to execute the script.

If a script should be enforced to run again, we can simply reset DownloadCount and ErrorCode to 0 and set Result and ResultDetails to nothing (empty string). After this we just restart the Microsoft Intune Management Extension Service (IntuneManagementExtension) and the script will rerun again on this device.


Can we schedule scripts?

No we do not have a run schedule at the moment. For example the agent does not execute the script every x hours. The agent checks every 60 minutes for new policies in the backend – during this no re-run of scripts occurs once a script is successful executed. If scheduling of scripts is needed I suggest to register the script as a scheduled tasks via PS commands. That’s the only way at the moment (as soon as we have something available for scheduling I will update the post accordingly).

Below a script to copy the scheduled script to a certain folder like C:\ProgramData\CustomScripts\myScript.ps1 and then register a scheduled task to run it periodically:

# Author: Oliver Kieselbach
# Date: 01/31/2018
# Description: install ps script and register scheduled task

# The script is provided "AS IS" with no warranties.

# define your PS script here
$content = @"
Out-File -FilePath "C:\Windows\Temp\test.txt" -Encoding unicode -Force -InputObject "Hello World!"

# create custom folder and write PS script
$path = $(Join-Path $env:ProgramData CustomScripts)
if (!(Test-Path $path))
 New-Item -Path $path -ItemType Directory -Force -Confirm:$false
Out-File -FilePath $(Join-Path $env:ProgramData CustomScripts\myScript.ps1) -Encoding unicode -Force -InputObject $content -Confirm:$false

# register script as scheduled task
$Time = New-ScheduledTaskTrigger -At 12:00 -Daily
$User = "SYSTEM"
$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ex bypass -file `"C:\ProgramData\CustomScripts\myScript.ps1`""
Register-ScheduledTask -TaskName "RunCustomScriptDaily" -Trigger $Time -User $User -Action $Action -Force

Here some documentation regarding PowerShell and scheduled tasks:

But you need to think about a strategy how to track all this or how to modify in the future. Once set you may want to have a modification script to change schedules or even delete it from the device again.


Integrated HealthCheck of Intune Management Extension agent?

The Intune Management Extension creates a scheduled task which will run a Health Check once a day.


If familiar with ConfigMgr and the ConfigMgr agent, there we have the same concept. The health check involves 4 files:

ClientHealthEval.exe and ClientHealthEval.exe.config: The binary which runs the health check.

HealthCheck.xml: The xml with all rules to run to perform the health check.

HealthReport.json: The json report with results of the rules defined by the xml.

The tests are defined in the xml file to check the agent registered service, startup type, service status and memory usage.


User interaction and timeout?

When using a scripts with the typical user interaction (like shown as an example in my first article Deep dive Microsoft Intune Management Extension – PowerShell Scripts), be advised that this script is getting executed and showing a dialog which blocks the script from finishing, until the user interacts with the dialog and then let the script finish. This can lead to timeouts and then scripts are getting marked as failed even when successfully ran.

The current default script timeout is 10 minutes for execution which is currently not adjustable.


If you find something missing or changed feel free to leave a comment. Thanks!