Intune – Backing up and Restoring your environment – New and improved

Update – 02/08/2023

Added support for GitLab

Update – 07/03/2023

Launched to provide a web front end to the script running in an Azure Automation runbook (More info here)

Update – 05/03/2023

Version 5.0 released, changelog:

  • Added extra parameters to trigger backup or restore via ID or Name without GUI at single policy level
  • Added support for webhook
  • Replaced write-host with write-output for use with Azure Automation Runbook
  • Added parameter for filename to skip grid-view on automated restore
  • Bypass script check when running on webhook
  • Github fix to cope with large files

Update – 24/02/2023

Version 4.0 released with a significant performance improvement

Update – 16/02/2023

Version 3.0 now released with added support for Azure DevOps Repo as well as GitHub, can be hard-coded, or via launch parameters

Update – 05/01/2023

Version 2.0 just released adding support for:

  • Policy Sets
  • Enrollment Configuration Policies
  • Device Categories
  • Device Filters
  • Branding Profiles
  • Admin Approvals
  • Intune Terms
  • Custom roles


If you are working with virtual machines, you would normally grab a snapshot before making any changes, so why can’t we do the same with Intune/AAD config?

I have previously looked at backing up your Intune environment with an automated big-bang approach (post here), which works well, but:

  • Restoring isn’t as easy as I would like
  • It’s a big-bang restore, everything is restored
  • The names are the same so need to dig around to find which is the new one
  • Versioning isn’t particularly straight forward
  • It uses the (whilst excellent) backup and restore module which relies on the Azure AD module (due for deprecation shortly)

With that in mind, I set about creating my own backup and restore script (yes, script, not module, it’s a single file) which includes:

  • Selective backup, you can pick what policies, Conditional Access, AAD Groups, Proactive Remediations, Script etc. you want to backup (via GUI)
  • A few extra backup options over the previous script
  • Backup to a single file
  • Backup to a Private Github Repo to give you all of the goodness that brings
  • Backups include Commit messages so you know why you made them
  • You can pick which restore point to use
  • You can pick what to restore from the selected restore point
  • Whatever you restore will have “-restore-DATE” at the start of the name so you know what it is and when it was restored (date of restore, not date of backup)!
  • All written using the new Graph SDK
  • Everything you need to set is in parameters to be able to use across multiple customers

So, after many hours of work, here it is. As usual you can grab it from GitHub Here or the PSGallery:

Install-Script -Name intune-backup-restore-withgui

First I’ll run through how the script works and then below how to configure the Github Repo, secret and automate it.


After configuring your Github repo, you need to run the script with the parameters required (available in get-help too)

intune-backup-restore-withgui.ps1 -type backup -selected some -reponame intunebackups -ownername my-github-account -token my-github-token

Type can be “backup” or “restore” depending on what you are doing

Selected can be “all” to simply grab or restore everything, or anything else will show the gui (it’s an if/else query)


When running in backup mode, you will see a GUI asking what you want to backup:

You can filter and select multiple via Ctrl/Shift click

Clicking Ok will write them to a single json file and upload it to your specified Github repo including the current date/time to reference when restoring


When running in Restore mode, you will first see a GUI asking which backup you want to restore from:

After selecting the backup, you will get another GUI showing the content within that backup where you can select what to restore

Click Ok and you’ll see the policies in your tenant with “Restored” at the start of the name

Creating Github Repo and Token

Create a repository, public will also work, but private probably makes more sense for backups

Now we need to create the token, in GitHub navigate to Settings (in the top right menu) – Developer Settings (at the bottom)

I’m using a Personal Access Token, but the API commands are all compatible with a GitHub App if you would prefer

Generate a new classic token and grab the output, it only needs Repo access

Azure AD App Registration

To use in Automated mode, you’ll need to create a secret:

Navigate to Azure AD and click App Registrations, then create a new Application

Give your application a name you will recognise and then select either Single Tenant or Multi-Tenant depending on your requirements, then click Register

Grab the Application (Client ID) from the Overview screen

Now click Authentication

Click Add Platform

You want Mobile or Desktop Application

Tick the 3 boxes and click Configure

Next, select API Permissions and click Add a Permission

Select Microsoft Graph

Select Application Permissions

Add these:

  • DeviceManagementApps.ReadWrite.All
  • DeviceManagementConfiguration.ReadWrite.All
  • DeviceManagementManagedDevices.ReadWrite.All
  • Domain.Read.All
  • Group.ReadWrite.All
  • Organization.Read.All
  • Policy.ReadWrite.ConditionalAccess
  • RoleManagement.ReadWrite.Directory
  • DeviceManagementServiceConfig.ReadWrite.All

You’ll see it’s still not finished, you need to click Grant admin consent

Last step here, click Certificates & secrets

Add a new secret

Add a name you’ll remember and set an expiry (I’m using 24 months because I’m lazy)

Copy the Secret Value somewhere safe, it won’t re-display it and if you lose it you’ll need to create a new one

Automating – Script Amendment

As you can see, normally this runs with a GUI and user authentication, but that won’t work in an automated task.

To automate, change this line to “yes”

$automated = "no"

Then populate the variables:

$selected = "all"

$reponame = "YOUR_REPONAME_HERE"



$clientid = "YOUR_AAD_REG_ID"

$clientsecret = "YOUR_CLIENT_SECRET"

$tenant = "TENANT_ID"

Automation Deployment

First, let’s create the Azure Automation Account:

Search for Azure Automation in your Azure Portal and create the account:

Now you’ll need to publish some PowerShell modules to the runbook (watch the versions on the Graph modules, it needs the non-preview ones):

For each module, click this button:

Create a new Runbook

Add the PowerShell code you’ve amended

Click Test Pane to make sure it’s worked

Now we need to publish it once it’s tested ok

Now we can add a schedule to run it weekly

That’s it, your basic weekly/daily/hourly backup is configured and will go direct to the Azure Storage Blob

If you want to get really clever, we can trigger a backup when a setting is changed within Intune using Event Hub. Follow my previous instructions here to create the event hub and link it to Intune and then use this logic app instead:

Select Event Hub as the trigger and use the connection string created before

For the action, find Azure Automation and set it to Create Job, then populate the details:

Then if any changes are made in Intune, you’ll see your Automation Job activate:

36 thoughts on “Intune – Backing up and Restoring your environment – New and improved”

  1. I get this error when I try to run a backup:

    Line |
    2293 | (Invoke-RestMethod -Uri $uri -Method put -Headers @{‘Authorization’=’ ā€¦
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | The format of value ‘Accept: application/json’ is invalid.

  2. Here is the command I used:

    intune-backup-restore-withgui.ps1 -type backup -selected some -reponame AppManifests -ownername [GITHUB USERNAME] -token [CLASSIC TOKEN]

  3. Hi Andrew,
    I had earlier setup the backup as per one of your previous article

    And as you commented and pointed out this one, i’m trying to setup this one.

    While setting up the App registration, i was able to find and grant all except the following 2 permissions:


    i looked through but do not see the above 2 anymore. Have they been renamed in Azure?

    Also, this article mentions how to setup the backup to be saved in Github repo. Is there also a way to backup & restore in Azure storage?
    If yes, then from which parts in the script am i supposed to enter/used in the Runbook?

    • Hi,
      Yes, looks like it’s had an update, these two should replace it I think:

      Line 2312 does the copy to GitHub so you could change that to store in an Azure blob

      For the restore, comment out lines 2335 to 2356 and add a line to read the azure file into a variable called $decodedbackup

  4. Hi Andrew,

    Wouldn’t more in the script need to be changed/modified?

    For example:
    -Line 102-120 (Variables) – as this isn’t pointing to Azure
    -Line 212 to 215 (Connect to Graph) – addition of the updated permissions
    -Line 2309 – this points to Github

    -Line 2312 – should this be replaced with the following for the connection & upload to Azure work correctly:

    ##Connect to AZURE
    $azurePassword = ConvertTo-SecureString $clientSecret -AsPlainText -Force
    $psCred = New-Object System.Management.Automation.PSCredential($clientID , $azurePassword)
    Connect-AzAccount -Credential $psCred -TenantId $tenantid -ServicePrincipal

    ##Convert Storage Account to lowercase (just in case)

    $storageaccount = $storageaccount.ToLower()

    ##Upload to Azure Blob
    $files = “$env:TEMP\IntuneBackup$date”
    $context = New-AzStorageContext -StorageAccountName $storageaccount -StorageAccountKey $storagekey
    Get-ChildItem -Path $files -File -Recurse | Set-AzStorageBlobContent -Container $storagecontainer -Context $context

    And about your comment to ‘add a line to read the azure file into into a variable called $decodedbackup’ – I’m not sure i follow. Could you please help & paste here what you mean ?

    • Lines 102-120 just won’t get used because you aren’t calling the variables, same with line 2309
      The permissions depends on how you are connecting to Azure, if you are using a shared key you just need headers and a webrequest

      That azure connection will work, but that’s uploading the IntuneBackup file which won’t currently exist so you’ll need to create that with the JSON content first.

      When performing a restore, you need to read the content into a variable called $decodedbackup so the rest of the script can use it

  5. Hi Andrew,

    Has your script changed or updated?
    the script lines you mentioned in these comments don’t match what you say.

    I’m a noob to scripting and i’m super confused with reading this article and trying to figure out your script.

    To keep it simple, i have the following questions to which i’d like to request clarity:
    1. Which part in the script do I need to modify exactly to automate the backups to get stored in Azure Storage? Can you please point out the specific lines in the script and what the changes would be?

    2. Is there an existing script of yours that has only Azure storage as the backup & restore platform and not Github repo.?

    I thank you in advance for your help & guidance.

    • Hi,
      Yes, the script is regularly updated with new features and bug fixes, in this case fixing some issues with larger settings catalog policies. The lines you need to amend haven’t been changed, but the line numbers will just have moved further down.
      The script grabs all policies into a variable which it then adds to a file and uploads to GitHub, this is the part you need to change so it uploads to Azure.

      With the restore, you need to remove the GitHub parts and grab your file from Azure storage into a variable called $decodedbackup

      I don’t have a version with Azure storage, the GitHub API gives me better control over versioning when using the GUI

      If you’re starting out with PowerShell, this book comes well recommended:
      “Learn PowerShell in a Month of Lunches”

  6. Hello, could you give the script a try with the new modules 1.21.0 released a few days ago, I’m getting various errors all over the place, however it does succeed in backing up the configuration to json on github, and the content of the json seems to be as expected for the configuration I exported. Thanks for all the effort!

  7. Looks a lot better now yes. Only a few errors, and now it really qucikly displays the list of config to select for backup.

    PS>TerminatingError(Invoke-MgGraphRequest): “GET
    HTTP/1.1 403 Forbidden

    PS>TerminatingError(Invoke-MgGraphRequest): “GET

    PS>TerminatingError(Invoke-MgGraphRequest): “GET

    I’m pretty sure I added the required rights, and I checked using graph explorer, the rights and queries, then compared with api permissions.

    If I just select canced after the list of config is displayed, and then cancel on the backup path it does not seem to by happy about that:
    At C:\Temp\intune-backup-restore-withgui-v211.ps1:2456 char:1
    + $profilesencoded =[Convert]::ToBase64String([Text.Encoding]::UTF8.Get …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentNullException
    Exception calling “GetBytes” with “1” argument(s): “Array cannot be null.
    Parameter name: chars”
    At C:\Temp\intune-backup-restore-withgui-v211.ps1:2456 char:1
    + $profilesencoded =[Convert]::ToBase64String([Text.Encoding]::UTF8.Get …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentNullException

  8. Hi Andrew,
    Thank you for your great work.
    I always get the following error when trying to restore. Regardless of whether Win365 user settings, normal config profiles, etc.

    ConvertFrom-Json : Invalid JSON primitive: .
    At C:\tmp\intune-backup-restore-withgui.ps1:2824 char:34
    + $profilelist2 = $decodedbackup | ConvertFrom-Json
    + ~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
    + FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand

    Cannot index into a null array.
    At C:\tmp\intune-backup-restore-withgui.ps1:2841 char:5
    + $value1 = ($profilelist3)[2]
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Do you have any idea what it could be?

    Thanks very much!

  9. In this case, I used Azure DevOps. However, I used to manually execute a pull request after every commit. But when I stopped doing that and instead selected the commit as a backup directly, the error no longer occurred.

    However, I’m getting the following error when backing up all of my Windows Autopatch configuration profiles (settings catalog):

    Invoke-MgGraphRequest : Cannot convert ‘System.Object[]’ to the type ‘System.Uri’ required by parameter ‘Uri’.
    Specified method is not supported.
    At C:\tmp\intune-backup-restore-withgui.ps1:1965 char:57
    + … $nextsettings = (Invoke-MgGraphRequest -Uri $policynextlink -Method …
    + ~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Invoke-MgGraphRequest], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.Graph.PowerShell.Authentication.Cmdlets.InvokeMgGraphReq

    It is the identical error for all Windows autopatch profiles

  10. Hi Andrew,

    What command would I use if I want to save the backup locally? Is that an option with this script, or are GitHub or Azure the only options for saving?

    Thanks in advance,

  11. Hello Andrew,

    We have created the backup account successfully using above details, but we are unable to identify where its creating its backup. As i am assuming it should be under Storage browser>Blob container>the name of your backup. But when i click it after running the runbook successfully getting no backup job created there. More details, we donot have anything currently in our intune tenant.

    • Hi, it will backup to whichever Git repo you tell it to use, either with a parameter or embedded in the script.
      You could also use my service if you want a better GUI

  12. Getting this error when running in Automation Account:

    It’s a policy
    System.Management.Automation.MethodException: Cannot convert argument “chars”, with value: “True”, for “GetBytes” to type “System.Char[]”: “Cannot convert value “True” to type “System.Char[]”. Error: “Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.”” —> System.Management.Automation.PSInvalidCastException: Cannot convert value “True” to type “System.Char[]”. Error: “Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.” —> System.InvalidCastException: Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.
    at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
    at System.Management.Automation.LanguagePrimitives.ConvertIConvertible(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
    — End of inner exception stack trace —
    at System.Management.Automation.LanguagePrimitives.ConvertIConvertible(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
    at System.Management.Automation.LanguagePrimitives.ConvertCheckingForCustomConverter.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
    at CallSite.Target(Closure , CallSite , Encoding , Object )
    — End of inner exception stack trace —
    at System.Management.Automation.ExceptionHandlingOps.ConvertToArgumentConversionException(Exception exception, String parameterName, Object argument, String method, Type toType)
    at CallSite.Target(Closure , CallSite , Encoding , Object )
    at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
    at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
    at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
    It’s a policy
    It’s a policy

    Then it continues to cycle through the policies but fails in the end:

    An error occurred stopping transcription: The host is not currently transcribing.

    • The transcript error will happen in automation. The other error is probably pagination and shouldn’t cause any issues.
      Have a look for any errors when exporting the JSON, it’s normally a permissions issue at the Git level

  13. Hello, the script is working as a charm!

    But when we run it in a runbook it gets this error:
    Thread failed to start. (Thread failed to start. (Exception of type ‘System.OutOfMemoryException’ was thrown.))

    There is a memory limit on the azure sandbox of 400Mb so I guess the workerthread exceeds this. Is there any way to divide the automated backup into pieces or any other solution you can think of?

    It is only one customer that this happens to and they have alot of configurations.


Leave a Comment