Azure automation runbooks are one of the chosen ways to provision Microsoft Teams or SharePoint sites inside Microsoft 365. A content of the runbooks normally favorites with PnP PowerShell and that’s well documented. But except from a scheduled start that pulls a request from a list how else can runbooks or (better!) corresponding jobs be started? (of course this way of starting is valid for any other runbook scenario)
In the past, there were a meanwhile deprecated Azure Automation SDK and a web hook scenario which could be used to start at runbook on demand. Now there are the new Azure Resource management Automation SDK and the parallel Rest API and this post describes how to use them.
Content
- The client
- Create
- Check for completion
- Security considerations
- Rest Client
- Create (Rest)
- Check for completion Rest
The client
First a client needs to be established, especially to validate security options. Having that client further operations can be full filled. In the following sample code an InteractiveBrowserCredential is used inside the DefaultAzureCredential. But as an alterative the EnvironmentCredential is prepared.
// For Dev and local env
Environment.SetEnvironmentVariable("AZURE_TENANT_ID", config["AZURE_TENANT_ID"]);
Environment.SetEnvironmentVariable("AZURE_CLIENT_ID", config["AZURE_CLIENT_ID"]);
Environment.SetEnvironmentVariable("AZURE_CLIENT_SECRET", config["AZURE_CLIENT_SECRET"]);
ArmClient client = new ArmClient(new DefaultAzureCredential(true)); // Enable interactive as well
Under security it is mentioned which roles or permissions are needed. Of course this also differs from the desired operations.
Create
An automation job will be created under the jobs of an existing automation account. So the access to this one has to be established first.
var automationAcc = client.GetAutomationAccountResource(new ResourceIdentifier(config["automationAccount"]));
var automationJobs = automationAcc.GetAutomationJobs();
After that, the job needs some parameters. First there is the name of the runbook, potentially some custom parameters needed by the runbook and finally the RunOn parameter.
var jobParameters = new Azure.ResourceManager.Automation.Models.AutomationJobCreateOrUpdateContent()
{
RunbookName = config["runbookName"],
RunOn = ""
};
// Using hard-coded parameters here
string alias = "TeamAlias";
jobParameters.Parameters.Add("displayName", "Team Name");
jobParameters.Parameters.Add("alias", alias);
jobParameters.Parameters.Add("teamDescription", "Team Description");
jobParameters.Parameters.Add("teamOwner", config["teamOwner"]);
var automationJob = automationJobs.CreateOrUpdate(Azure.WaitUntil.Started, $"Creation of {alias}", jobParameters);
Having that the job can be started. It must be clear that this is only the start with no guarantee on the result. So another loop where to detect if the job completed successfully makes sense.
Check for completion
int count = 0;
while (count < 10)
{
var newAutomationJob = automationAcc.GetAutomationJob($"Creation of {alias}");
if (newAutomationJob.Value.Data.Status == AutomationJobStatus.Completed)
{
Console.WriteLine($"Job Ended {automationJob.Value.Id}");
break;
}
if (newAutomationJob.Value.Data.Status == AutomationJobStatus.Failed || newAutomationJob.Value.Data.Status == AutomationJobStatus.Stopped)
{
Console.WriteLine($"Job Ended unsuccesful {automationJob.Value.Id}");
break;
}
count++;
Thread.Sleep(30000);
}
The counter and sleep timer are assumed values here. But the pattern is pretty much clear. In a loop the job is checked for completeness or failed state and the output is given.
Security considerations
How does the whole operation work from a security perspective? The caller can be a user (interactively) or an app (unattended): DefaultAzureCredential works best with it inside the code.
What is mainly necessary is the Role Assignment (or a subset of permissions): Automation Job Operator for any kind of user or app registration trying to fulfill this kind of service operation.

In case you want to continue inside the runbook job on behalf of the starting user there is the problem that the caller is not identifiable inside the runbook (token) except explicitly transported through parameters.
When using the rest API you must understand that it cannot be done directly from a client context because Azure Resource Management Rest API does not support CORS. So coming from a client context such as a SharePoint framework (SPFx) e.g. you first need to call a back end process and there you have the choice to use .Net or Rest.
Rest Client
Alternatively, to the .Net SDK there is also the possibility to perform the operations with the Rest API. First a Http Client needs to be established with a Bearer Token based on a simple Entra ID app registration. For Azure there is no granular permission model in Entra ID (see scope user_impersonation below). This is handled inside the resource model so here only a simple permission is set and later the app is given permissions role based (RBAC) as seen under security.

var tokenCredential = new DefaultAzureCredential(true);
var accessToken = tokenCredential.GetToken(new Azure.Core.TokenRequestContext(["https://management.azure.com/user_impersonation"])).Token;
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
Having a client and a token here the request can directly start, assuming a subscription, resource group and account name are given:
Create (Rest)
string groupAlias = "TeamAlias";
JobStartRequest request = new JobStartRequest()
{
properties = new JobProperties()
{
runbook = new RunbookProperties()
{
name = config["runbookName"]
},
parameters = new JobParameters()
{
groupAlias = groupAlias,
displayName = "Team Name",
teamDescription = "Team Description",
teamOwner = config["teamOwner"]
},
runOn = ""
}
};
var jsonRequest = JsonSerializer.Serialize(request);
string jobStartUrl = $"https://management.azure.com/subscriptions/{config["subscriptionID"]}/resourceGroups/{config["resourceGroupID"]}/providers/Microsoft.Automation/automationAccounts/{config["automationAccount"]}/jobs/Creation{groupAlias}?api-version=2023-11-01";
var jobStartReq = new HttpRequestMessage(HttpMethod.Put, jobStartUrl);
jobStartReq.Content = new StringContent(jsonRequest, Encoding.UTF8);
jobStartReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var jobStartResult = await client.SendAsync(jobStartReq);
jobStartResult.EnsureSuccessStatusCode();
var jobStartContent = await jobStartResult.Content.ReadAsStringAsync();
var createdAutomationJob = JsonSerializer.Deserialize<AutomationJob>(jobStartContent);
string jobName = createdAutomationJob.name;
Although the client above was easier to initialize here it looks like a bit more code. But take into account that now using rest you are responsible for deserializing the results. For deserializing I used reduced classes orientating on the Rest documentation.
Check for completion Rest
Also with Rest a check for completion of the job can be done of course. The the pattern is the same as above. Only the calls are slightly different.
string jobName = createdAutomationJob.name;
int count = 0;
while (count < 10)
{
string jobCheckUrl = $"https://management.azure.com/subscriptions/{config["subscriptionID"]}/resourceGroups/{config["resourceGroupID"]}/providers/Microsoft.Automation/automationAccounts/{config["automationAccount"]}/jobs/{jobName}?api-version=2023-11-01";
var checkReq = new HttpRequestMessage(HttpMethod.Get, jobStartUrl);
var jobCheckResult = await client.SendAsync(checkReq);
jobCheckResult.EnsureSuccessStatusCode();
var ceckContent = await jobCheckResult.Content.ReadAsStringAsync();
var newAutomationJob = JsonSerializer.Deserialize<AutomationJob>(ceckContent);
if (newAutomationJob.properties.status == "Completed")
{
Console.WriteLine($"Job Ended {newAutomationJob.properties.jobId}");
break;
}
if (newAutomationJob.properties.status == "Failed" ||
newAutomationJob.properties.status == "Stopped")
{
Console.WriteLine($"Job Ended unsuccesfully {newAutomationJob.properties.jobId}");
break;
}
count++;
Thread.Sleep(30000);
}
Console.ReadLine();
Using the job name from the original start it now gets requested again. The job object is then checked for its status and if it’s not yet done in any case the loop continues.
As usual there is also a full sample code repository on GitHub illustrating the shown things in .Net SDK as well as in Rest API.
|
Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 Development. He loves SharePoint Framework but also has a passion for Microsoft Graph and Teams Development. He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich. In 2021 he received his first Microsoft MVP award in M365 Development for his continuous community contributions. Although if partially inspired by his daily work opinions are always personal. |

















