Skip to Main Content

As soon as modern sites were introduced for SharePoint Online, there has long been the desire for custom site templates throughout an organization. When you get started from the SharePoint home page you have the ability to create a modern Team and Communication site with some slight variations. Administrators, do have the ability to register their own custom templates by augmenting these out of the box sites with a combination of Site Scripts and Site Designs. You can think of site scripts as additional actions you want taken once the original site has been provisioned and Site Designs as the wrapper that identifies what scripts to run against a particular template which is shown to the user in the native site creation experience. You can read more in Site Designs and Site Scripts in the official documentation here: https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/site-design-overview.

One of my favorite actions you can use in a site script is the ability to trigger a Microsoft Flow to do additional actions, such as adding a list item to a site directory list with the site owner or in many cases using the pattern to call an Azure Function which allows an administrator to run additional code, or PowerShell, to do enhanced provisioning on the newly created site. The Azure Function pattern is typically used when you want to perform actions that either cannot be done through site designs or processing more actions than a site design allows (there is currently a limit of 30 site script actions for a site design). There is a great write-up walking through using this pattern and setting up what you need with the Azure Function, Storage Queue and so forth here: https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/site-design-pnp-provisioning which I have referenced often. My only issue with this pattern is that using PowerShell with Azure Functions is only an “experimental” feature and support for it will not be continued with the next version as stated here: https://docs.microsoft.com/en-us/azure/azure-functions/supported-languages. As the supporting documentation says, if you want to utilize PowerShell in this fashion in the future you should consider using Azure Automation, but the question is, how can we use the similar pattern to Azure Functions for site provisioning by using Azure Automation. I’m glad you asked, because that is the entire purpose of this blog post.

To get started, you will need an Azure Subscription, just like you would if you were using Azure Functions, but in this case you are going to create an Automation Account, and if you want to use pre-saved templates for the PnP Provisioning engine you are also going to want to have a storage account to store the XML definition in a blob store. To create a new Automation Account you first Choose + Create a resource. Next you select Automation from Management Tools in the Azure Marketplace and then you fill in the necessary fields such as a Name, Subscription, Resource group and Location. This will be a container used for grouping scripts that you want to manage.

Once your Automation Account has been created you should see something similar to this screen:

Screenshot: Azure Automation New Account

Before you get started creating your first Runbook, yes that’s what your individual scripts will end up being called, you are going to want to consider performing a few initial tasks. First, if you want to use any of the PnP PowerShell commands, you will need to import that module. Good news for you, importing these are much easier than in Azure Functions. From your Automation Account, select Modules in the left navigation under Shared Resources (you may notice a bunch of other options here, we will be coming back to those). You will notice the available modules that have already been loaded for you, of course most of them are Azure based, but select Browse Gallery as seen here:

Screenshot: Azure Automation Browse Account Modules

If you do a search for PnP you will find the same module you may have imported from the PSGallery called SharePointPnPPowerShellOnline. Select that module and then press the Import button to start the process of importing the module into the Automation account for all of your runbooks to use.

Screenshot: Azure Automation Import Account Modules

Once your import is completed, you may want to look into storing shared credentials, or variables to use across your runbooks. In my case I will be storing variables for my Client Id and Client Secret for use of authenticating to SharePoint Online and a Storage Account connection string and container name for use authenticating to the Azure blob store. After your shared credentials and variables are set, select Runbooks under Process Automationto see the listing of runbooks that are associated with the Azure Automation Account. Right now your list is empty so time to create your first runbook by selecting + Add a runbook.

From the Add Runbook screen select either Quick Create to start from scratch or Import from an existing runbook to import an existing PowerShell (.ps1) file. In this example we will start one from scratch and fill out a Name, Runbook type of PowerShell and Description like seen here:

Screenshot: Azure Automation Account New Runbook

Once your runbook is created, this is where all the magic happens. Here is an example of an Azure Automation runbook I created to either start from a webhook (more on that to come in a bit) or manually for testing. The following script connects to Azure blob storage account to grab a PnP Provisioning template from a name based on the $fileName input parameter, then connects to SharePoint Online to apply the template to a site based on an the $url input parameter as well as creating a new modern page called News, while also adding a text control and web part along with a navigation node for the new page:

[CmdletBinding()]
Param(
 [object]$WebhookData,
 [string]$url,
 [string]$fileName)

 if ($WebhookData)
{
 Write-Output ("Starting runbook from webhook")
 # Collect properties of WebhookData
 $WebhookName = $WebHookData.WebhookName
 $WebhookHeaders = $WebHookData.RequestHeader
 $WebhookBody = $WebHookData.RequestBody

 # Collect individual headers. Input converted from JSON.
 $From = $WebhookHeaders.From
 $InputBody = (ConvertFrom-Json -InputObject $WebhookBody)
 Write-Verbose "WebhookBody: $InputBody"

 $url = $InputBody.url
 $fileName = $InputBody.fileName
 Write-Output -InputObject ('Runbook started from webhook {0} by {1}.' -f $WebhookName, $From)
} else {
 Write-Output ("Starting runbook manually")
}

$clientId = Get-AutomationVariable -Name 'AppClientId'
$clientSecret = Get-AutomationVariable -Name 'AppClientSecret'

Write-Output ("Connecting to SharePoint Online '" + $url + "'")
Connect-PnPOnline -AppId $clientId -AppSecret $clientSecret -Url $url
$web = Get-PnPWeb
Write-Output ("[[ Connected to SPO ]]")

$connString = Get-AutomationVariable -Name 'StorageConnString'
$containerName = Get-AutomationVariable -Name 'StorageContainer'

Write-Output ("Connecting to Azure Storage")
$storageAccount = New-AzureStorageContext -ConnectionString $connString
Write-Output ("[[ Connected to Azure Storage")

Write-Output ("Getting file '" + $fileName + "' from Azure Blob Store")
Get-AzureStorageBlobContent -Blob $fileName -Container $containerName -Destination ("c:\temp\" + $fileName) -Context $storageAccount
Write-Output ("[[ File '" + $fileName + "' saved ]]")

Write-Output ("Applying Provisioning Template")
Apply-PnPProvisioningTemplate -Path ("c:\temp\" + $fileName)
Write-Output ("[[ Provisioning Template Applied ]]")

Write-Output ("Creating News Page")
$newPage = Add-PnPClientSidePage -Name "News" -LayoutType Article -Publish
$newText = Add-PnPClientSideText -Page "News" -Text "This is where you can find all of your news"
$newWP = Add-PnPClientSideWebPart -Page "News" -DefaultWebPartType NewsReel

$newsUrl = $web.Url + "/SitePages/News.aspx"
$newNode = Add-PnPNavigationNode -Location QuickLaunch -Title "News" -Url $newsUrl
Write-Output ("[[ News Page Created ]]")

Most of the pieces in the script above should look familiar if you have used the PnP PowerShell commands before, but the things I want to point out are like the Get-AutomationVariable command which will pull the variables set on the Azure Automation Account (even if the values are encrypted). Since this runbook will be processed in an isolated environment you will also notice that the script downloads the PnP template definition file to the isolated c:\tempdirectory in the instance so that we can then use it during Apply-PnPProvisioningTemplate command. Once you publish the runbook you might be wondering, how you can add this to the process for site designs since it is still very segregated.

Wait for it…

Ok, now we will add the empowerment of webhooks with the runbook to allow us to call into the system and run our newly created script. Back on the runbook screen you can select the Webhook button, or select Webhooks from the Resources section in the left navigation and select + Add Webhook. From the Create a new webhook screen, give it a Name and define the Expiration date and time for the webhook. By default the webhook will expire one year from when you create it, but you can adjust the expiration later if need be. Next you really want to COPY out the URL for the webhook, because as soon as you create the webhook there is no way to get back to the URL. Here is a sample webhook creation screen:

Screenshot: Azure Automation Account New Webhook

You don’t have to add any default values into the parameters on the Parameters and run settings screen if you don’t need to, but you do need to go into it to select OK and then create your webhook.

From there you are ready to Create your Flow that will be called by the Site Design. You will start with the When a HTTP request is received trigger and insert in the following JSON markup:

{
 "type": "object",
 "properties": {
 "webUrl": {
 "type": "string"
 },
 "parameters": {
 "type": "object",
 "properties": {
 "fileName": {
 "type": "string"
 },
 "event": {
 "type": "string"
 },
 "product": {
 "type": "string"
 }
 }
 }
 }
}

The only difference to this markup and the one from the PnP Azure Function documentation is that I’m registering an additional parameter called fileNameso that our site designs can individually call to separate PnP Provisioning templates hosted in our Azure blob store. Next you will have a single action HTTP call where you will use the POST method, the Uri is the URL you copied out of your new webhook, add a few headers in to use in the script to identify where (From) and when (Date) the incoming call is coming from and use the parameters passed from the Site Script that will be registered that will end up looking similar to this:

Screenshot: Azure Automation Account Flow

When you have your Flow created, go ahead and save, which will give you the URL that will need to be registered in your Site Script triggerFlow action. Next it’s time to create your Site Script and Site Design which is pretty well documented elsewhere. I utilize the PnP PowerShell commands for that as well using Add-PnPSiteScript and Add-PnPSiteDesign, so the following is the markup for a single action Site Script that I then register to my site design to a Communication Site:

{
 "$schema": "schema.json",
 "actions": [
 {
 "verb": "triggerFlow",
 "url": "https://prod-59.westus.logic.azure.com:443/workflows/888a22fce7d5...",
 "name": "Apply Template",
 "parameters": {
 "fileName":"PnPTemplateDefinition.xml",
 "event":"Site Design",
 "product":"SharePoint Online"
 }
 }
 ],
 "bindata": {},
 "version": 1
}

My custom Site Design is called SPFx Navigation which uses the PnPTemplateDefinition.xml PnP Provisioning file located in my Azure blob store that is used to register a SharePoint Framework application extension, but could as well create a bunch of site columns, lists, items, etc. as seen here:

Screenshot: Azure Automation Create Form Site Design

When the site is created you will notice the fly out stating that our Site Script actions are being triggered, for which we can verify by looking at our run history in Flow as well as Recent Jobs associated with our runbook in Azure. If we go to look at the output from our completed runbook job we will notice the output from the runbook script to show how the script progressed through as seen here:

Screenshot: Azure Automation Account Runbook Output

With all that said, stand up and walk around the office in celebration because you survived this blog post. We now have a new site successfully created using Azure Automation powered by the PnP Provisioning process which empowers users to create sites based on custom templates using the native UI in SharePoint Online.

Screenshot: Azure Automation Account New Site Success