G’day! Hope you’re all well. Bit of a fun one today. Ever fancied your users logging in to a frosty wallpaper in December, something sunny come June and something festive over Christmas, all without you having to touch a thing? In this post, I’ll be rotating an Intune wallpaper and lock screen image with the seasons using an Azure Storage account, Azure Automation and a bit of Graph API, set it up once and it just ticks over in the background year after year.
I’m using the seasons of the year for the sake of this demo, but you’re not stuck with that. You could use the exact same setup to rotate images for a corporate promotion, or swap them out monthly based on company news, it’s just a case of changing the image names and schedules to suit your needs.
The problem with a static Intune wallpaper
Setting a corporate desktop wallpaper and lock screen through the Intune Settings Catalog is easy enough, the catch is that the image you set is the image you’re stuck with until somebody remembers to change it. Nobody ever does. Which is how you end up with snow on the lock screen in July.
There’s also a technical gotcha that trips a lot of people up, the Personalization CSP only downloads the image again when the URL changes. So if you were thinking of overwriting a single current.png in blob storage every few months, I’m afraid Windows will happily carry on showing the cached copy. The URL in the profile has to change, which means the profile itself has to change, and that’s where Azure Automation and Graph come in.
How it all fits together
There are four moving parts:
- An Azure Storage account holding the seasonal images in a blob container.
- A Settings Catalog profile in Intune that sets the Desktop Image URL and Lock Screen Image URL.
- An Azure Automation runbook, kicked off by a schedule for each season (plus Christmas if you fancy it) that fires once a year, which uses Graph to swap the URL’s in the profile.
- Devices picking up the new URL’s the next time they check in with Intune and downloading the new images.
The Automation account authenticates with a system assigned managed identity, so there’s no client secret or certificate to keep renewing.
Prerequisites
- An Azure subscription where you can create a Storage account and an Automation account
- Intune Administrator access
- Windows 11 (or 10) Enterprise or Education on your devices
- Your wallpaper images in JPG or PNG, I would recommend 3840×2160 (more on that in the gotchas)
- Privileged Role Administrator or Global Administrator for the one off Graph permission grant
⚠️ Important
The Personalization CSP isn’t supported on Windows Pro. Intune won’t flag it as an error either, the settings just show as Not applicable in the per-setting status, which is easy to miss if you only glance at the profile overview. Enterprise or Education only!
Hosting the images in Azure Storage
First up, the devices need somewhere to download the images from. Create a Storage account (Standard LRS is plenty, this is hardly mission critical data), add a container called wallpapers and upload your images: spring.png, summer.png, autumn.png and winter.png, plus christmas.png if you’re joining in with the festivities. Keep the names all lowercase, more on why in the gotchas.
I’m using PNG throughout but JPG works just as well, just keep every image in the same format, as the runbook builds the URL’s from a single file extension that you’ll set in one variable later on. Once uploaded, your container should look something like this:

If you just want to follow along and haven’t got any images to hand yet, I’ve put together a set of 4K wallpapers with the Modern Workspace Hub logo on, already named to match the runbook. Feel free to grab them here:
📝 Note
Any service that gives you a stable, direct download URL will do the job here, OneDrive, your own web server, whatever suits. I’ve gone with Azure Storage as it’s cheap and sits nicely alongside the Automation account, but the rest of the post works the same whichever you use.
You’ve then got two options for making the images reachable. I’ve gone with Option B for this demo so that’s what the screenshots show, Option A is only a couple of settings anyway.
Option A: public container
The simple route, anyone with the URL can view the images, which for wallpaper is usually fine:
- On the Storage account, go to Settings, then Configuration and set Allow Blob anonymous access to Enabled.
- On the
wallpaperscontainer, set the anonymous access level to Blob (anonymous read access for blobs only).
Your image URL’s will look like so:
https://yourstorageaccount.blob.core.windows.net/wallpapers/winter.png
⚠️ Important
Keep this container just for wallpapers and don’t go putting anything sensitive in it.
Option B: private container with a SAS token
If your security team (or your own conscience) won’t allow anonymous access, keep the container private and use a read only SAS instead.
First, on the Storage account go to Settings, then Configuration and check Allow storage account key access is set to Enabled, you’ll need it for the SAS in a minute. Leave Allow Blob anonymous access on Disabled. If you went with Option A, this is the same page you’d enable anonymous access on:

Next, open the wallpapers container and select Shared access tokens. The Signing method drop-down gives you two options, and the one you pick matters here:
| Signing Method | Signed with | Longest Expiry |
|---|---|---|
| Account key | The Storage account’s access key | No limit, years if you want |
| User delegation key | A temporary key from your Entra ID sign-in | 7 days |
⚠️ Important
Go with Account key. A user delegation key SAS can’t last more than 7 days, so the next season’s image would fail to download and you’d be none the wiser until the wallpaper didn’t change. Being the admin, you’ll more than likely have the rights to set a long expiry. Bear in mind if anyone rotates that account key, the SAS stops working too.
Set Permissions to Read only, pick a long expiry (a few years out, and stick a reminder in your calendar), leave Allowed protocols on HTTPS only and select Generate SAS token and URL, here’s mine:

Copy the Blob SAS token somewhere safe, it goes into the Automation account later on. Your URL’s then become:
https://yourstorageaccount.blob.core.windows.net/wallpapers/winter.png?<sas-token>
📝 Note
To be clear on what we’re generating, it’s a SAS scoped to that one container with read permission only, so it can read the images and nothing else. Don’t ever hand out the storage account access keys for something like this, they give full control over the whole account.
Keep your storage account name (and SAS token if you went with Option B) to hand, you’ll need them in the next bit.
Creating the Intune wallpaper profile
Over in the Intune admin centre, go to Devices, then Configuration, select Create, then New policy, choose Windows 10 and later for the platform and Settings catalog for the profile type.
Give it a name, I’ve gone with PROD – Seasonal Wallpaper and Lock Screen, note it down exactly as the runbook finds the profile by name. Select Add settings, search for Personalization and tick both Desktop Image URL and Lock Screen Image URL, then enter in the URL of your current season’s image for both (or different images for each if you’re feeling fancy).
If you went with Option A, that’s just the plain blob URL:
https://yourstorageaccount.blob.core.windows.net/wallpapers/autumn.png
For Option B, you don’t need to build any of the SAS part yourself, it’s the Blob SAS token you copied earlier, all of it, from sp= right through to the end of the signature. Stick a ? on the end of the image URL and paste the token straight after it (if your token already starts with a ?, don’t add a second one). It should end up looking like this:
https://yourstorageaccount.blob.core.windows.net/wallpapers/autumn.png?sp=r&st=2026-10-30T11:39:50Z&se=2030-10-30T19:54:50Z&spr=https&sv=2026-02-06&sr=c&sig=<signature>
Don’t paste in the Blob SAS URL from the portal as is, as you generated the SAS on the container it points at the container rather than an image. Add the image name in before the ? yourself.
Assign it to a test device group and create the profile, it should look something like this:

Test it on a device before going any further and ensure all is well. In my lab it took roughly 10 minutes to show up after forcing a sync. The new wallpaper doesn’t always kick in mid-session, so if the profile shows as succeeded but the desktop hasn’t changed, sign out and back in again (lock the device to check the lock screen). If it still doesn’t show up, check the Windows edition before blaming anything else.
💡 Tip
To see what the device has actually received, have a look in HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PersonalizationCSP. DesktopImageUrl and LockScreenImageUrl show the URL’s from the profile, DesktopImageStatus and LockScreenImageStatus should be 1 once the image has downloaded, and DesktopImagePath shows where the local copy lives. Handy for checking it’s picked up a new season without waiting on a user to log back in.
The Automation account and Graph permission
Create an Automation account, then under Account Settings, Identity, make sure the system assigned managed identity is set to On and copy the Object ID, like so:

The managed identity needs the DeviceManagementConfiguration.ReadWrite.All application permission on Microsoft Graph. You can’t grant Graph app roles to a managed identity through the portal, so it’s a quick bit of PowerShell from your admin workstation, swap in the Object ID you copied:
<#
.SYNOPSIS
Grants DeviceManagementConfiguration.ReadWrite.All to an Automation account managed identity.
.DESCRIPTION
Assigns the Microsoft Graph application permission needed by the seasonal
wallpaper runbook to the Automation account's system assigned managed identity.
.NOTES
Author: Alex Durrant
Version: 1.0
#>
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"
$miObjectId = "<your managed identity Object ID>"
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq "DeviceManagementConfiguration.ReadWrite.All" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miObjectId `
-PrincipalId $miObjectId `
-ResourceId $graphSp.Id `
-AppRoleId $appRole.Id
PowerShellOnce that’s run, you can check it in Entra ID under Enterprise applications, change the application type filter to Managed Identities, open the Automation account and select Permissions:

One permission and nothing to expire, this is why I love managed identities.
If you went with Option B, now’s the time to store that SAS token. In the Automation account go to Shared Resources, then Variables, select Add a variable, call it WallpaperSasToken, set the type to String, paste in the token and set Encrypted to Yes:

The runbook
Create a new runbook in the Automation Account, I’ve called mine Update-SeasonalWallpaper. Set the Runbook type to PowerShell and pick the newest PowerShell 7 Runtime version in the list, which was 7.2 in my case. If your account has been switched over to the newer Runtime environments experience you’ll get PowerShell 7.4 instead, the script works on either. Az.Accounts comes with the PowerShell 7 runtimes so there shouldn’t be anything to import, but check under Modules if the script complains.
The script takes one parameter, the season (or theme) to apply, which lines up with an image name in your container. It finds the profile through Graph, swaps in the new URL’s and writes the profile back. If the URL’s are already correct, it exits without touching anything.
Graph hands results back in pages, so the script follows the @odata.nextLink trail until it has every policy, then picks yours out by name. I’d steer clear of using $filter on this endpoint, it can return empty pages with a nextLink before it gets to your policy, and if you only read the first page you’ll get a false “not found”.
⚠️ Important
Going with Option B? Remove the # from the start of line 27 ($sasToken = Get-AutomationVariable -Name "WallpaperSasToken"). Leave it commented out and the runbook writes the URL’s without the SAS token, so devices get refused when they try to download the images, and nothing in the runbook output will tell you. Option A folks, leave it as it is.
Copy and paste the below into the runbook and change the variables at the top to suit your environment:
<#
.SYNOPSIS
Updates the Intune wallpaper profile with the image for the given season.
.DESCRIPTION
Triggered by Azure Automation schedules (one per season), each passing in the season
(or theme) to apply. Updates the Desktop Image URL and Lock Screen Image URL
values in the target Settings Catalog profile via Microsoft Graph.
.PARAMETER Season
The season or theme to apply. Must match an image name in the container, e.g. winter.
.NOTES
Author: Alex Durrant
Version: 1.0
#>
param(
[Parameter(Mandatory = $true)]
[string]$Season
)
# Variables, change these to suit your environment
$policyName = "PROD - Seasonal Wallpaper and Lock Screen"
$baseUrl = "https://yourstorageaccount.blob.core.windows.net/wallpapers"
$fileType = "png" # change to jpg if that's what you've uploaded
# Option B only, uncomment if using a private container with a SAS token
# stored as an encrypted Automation variable
# $sasToken = Get-AutomationVariable -Name "WallpaperSasToken"
$imageUrl = "$baseUrl/$($Season.ToLower()).$fileType"
if ($sasToken) { $imageUrl = "${imageUrl}?$($sasToken.TrimStart('?'))" }
Write-Output "Applying the $Season image to the policy"
# Authenticate with the managed identity and grab a Graph token
Connect-AzAccount -Identity | Out-Null
$token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token
# Newer versions of Az.Accounts return the token as a SecureString
if ($token -is [securestring]) { $token = $token | ConvertFrom-SecureString -AsPlainText }
$headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$graph = "https://graph.microsoft.com/beta"
# Graph returns results in pages, keep following @odata.nextLink until there are none left
function Get-GraphPagedResult {
param([string]$Uri)
$results = @()
while ($Uri) {
$page = Invoke-RestMethod -Uri $Uri -Headers $headers
$results += $page.value
$Uri = $page.'@odata.nextLink'
}
return $results
}
# Find the Settings Catalog policy by name (matched here rather than with $filter,
# which can return empty pages on this endpoint)
$policy = Get-GraphPagedResult -Uri "$graph/deviceManagement/configurationPolicies" |
Where-Object { $_.name -eq $policyName } | Select-Object -First 1
if (-not $policy) { throw "Policy '$policyName' not found." }
$policyId = $policy.id
# Retrieve the current settings (paged for the same reason)
$settings = Get-GraphPagedResult -Uri "$graph/deviceManagement/configurationPolicies('$policyId')/settings"
# Update both Personalization URL values
$changed = $false
foreach ($setting in $settings) {
$defId = $setting.settingInstance.settingDefinitionId
if ($defId -like "*personalization_desktopimageurl" -or $defId -like "*personalization_lockscreenimageurl") {
if ($setting.settingInstance.simpleSettingValue.value -ne $imageUrl) {
$setting.settingInstance.simpleSettingValue.value = $imageUrl
$changed = $true
}
}
}
if (-not $changed) {
Write-Output "URLs already set for $Season, nothing to do."
return
}
# Write the policy back with the updated settings
$body = @{
name = $policy.name
description = $policy.description
platforms = $policy.platforms
technologies = $policy.technologies
roleScopeTagIds = $policy.roleScopeTagIds # keeps your scope tags, a PUT resets them otherwise
settings = $settings
} | ConvertTo-Json -Depth 50
Invoke-RestMethod -Method PUT -Uri "$graph/deviceManagement/configurationPolicies('$policyId')" -Headers $headers -Body $body
Write-Output "Policy updated successfully to $Season."
PowerShell📝 Note
The configurationPolicies endpoint sits under Graph beta. It’s what the Intune portal itself uses behind the scenes and it’s been stable for years, but as with anything beta, keep an eye on it.
Before publishing the runbook, open the Test pane, enter in a season for the Season parameter and select Start. Use a different season to the one already in your profile, otherwise the runbook will just tell you there’s nothing to do, for me it was summer:

Then pop back over to the profile in Intune and give it a refresh, both URL’s should now point at the new image:

If all is well, publish it. Run the test once more with your actual current season if you don’t want your test devices sat on a summer wallpaper in the middle of autumn.
Scheduling it
Last bit. Automation schedules don’t have a yearly option, so the trick is a schedule per image that recurs every 12 Months, all linked to the same runbook.
Once the runbook’s published, open it and select Link to schedule, then Link a schedule to your runbook, then Add a schedule. Fill in the New Schedule pane like so, I’ve used spring as the example:
| Setting | Value |
|---|---|
| Name | Seasonal Wallpaper – Spring |
| Starts | 1 March, 06:00 |
| Time zone | <<Your time zone>> |
| Recurrence | Recurring |
| Recur every | 12 Months |
| Set expiration | No |
Leave Monthly occurrences alone. I’ve gone with 06:00 so the change is done and dusted before anyone logs on. Double check the time zone too, it defaults to whatever your browser reports, which isn’t always what you’d expect. The start date has to be in the future, so for any season that’s already started this year, put next year’s date in.

Select Create, then back on the Schedule pane select Parameters and run settings, enter in spring for Season and select OK, then OK again to link it:

Rinse and repeat for the rest. Here’s mine, UK meteorological seasons with a festive bonus:
| Starts on | Recurs every | Season |
|---|---|---|
| 1 March | 12 months | spring |
| 1 June | 12 months | summer |
| 1 September | 12 months | autumn |
| 1 December | 12 months | christmas |
| 2 January | 12 months | winter |
The dates only live in the schedules, so it’s entirely up to you. In the Southern Hemisphere? Flip them around. Fancy a Halloween wallpaper for October, or something for a company milestone? Upload the image, add a schedule with the matching name and you’re done, no code changes needed.

One trade off with this approach, if a scheduled run is ever missed, the wallpaper stays on the previous image until the next schedule fires, nothing corrects itself in between. In practice Automation schedules are very reliable and you can always start the runbook manually, which is worth doing now with your current season to get everything in line.
Devices pick up the new URL’s the next time they check in with Intune, usually within 8 hours, and because the URL has changed, the CSP downloads the new image instead of serving up the cached copy.
Gotchas
- Users can’t change their wallpaper. Setting these CSP’s locks personalisation down, so make sure you’ve had that conversation with the business first.
- Graph Explorer and paging. If you’re testing filters in Graph Explorer, an empty
valuewith an@odata.nextLinkjust means there are more pages to go. Either page through it, or grab the policy GUID from the address bar in the Intune portal and skip the search entirely. - Lowercase file names. Blob names are case-sensitive (unlike Windows), and the runbook lowercases the season it’s given. Upload
Autumn.pngand the profile ends up pointing atautumn.png, which doesn’t exist, so the wallpaper simply doesn’t change and nothing tells you why. I hit this problem in my lab and wondered what was going on! - Image size. One image gets stretched \ cropped to fit every screen, so go with 3840×2160 (4K, 16:9), it scales down cleanly to 1080p and 1440p. Keep the main subject in the middle of the image so it survives cropping on 3:2 screens like the Surface range. Scaling down always looks fine, scaling up never does.
- SAS expiry and key rotation (Option B). When the token expires, or the storage account keys are rotated (the SAS is signed by an account key behind the scenes), wallpapers quietly stop downloading and nothing will tell you. Set a long expiry and put a reminder in your calendar to renew it and update the Automation variable.
💡 Tip
This same Automation and Graph pattern works for more than wallpapers. Any Settings Catalog value that needs to change on a schedule, swap out the setting definition IDs in the runbook and away you go.
Frequently asked questions
Why is my Intune desktop wallpaper not applying? Nine times out of ten it’s the Windows edition. The Personalization CSP does nothing on Windows Pro, it needs Enterprise or Education. If the edition’s right, check the device can actually reach the image URL, and if you’re using a SAS token, that it hasn’t expired.
Can I set the Intune lock screen image as well as the desktop wallpaper? Yes, both live in the same Personalization category in the Settings Catalog, Desktop Image URL and Lock Screen Image URL, and the runbook in this post updates both in one go.
Why doesn’t overwriting the image file update my devices? The Personalization CSP only downloads the image again when the URL changes. Overwrite winter.png behind the same URL and devices will keep showing the cached copy, which is why this rotates the URL in the profile instead of the file in storage.
How quickly do devices pick up the new wallpaper? The next time they check in with Intune, usually within 8 hours. For testing, you can kick off a sync from the Company Portal or the Intune admin centre to hurry one along.
Wrapping up
And that’s it, an Intune wallpaper that looks after itself. It’s only a small thing, but it’s the sort of thing end users actually notice.
While you’re tidying up Intune, I’ve also written about bulk renaming Intune policies, handy if your profile names are a bit all over the place.
As always, test on a pilot group before letting it loose on production. If you’ve got any questions or run into any issues, drop a comment below.
Happy decorating! 👌



