I recently came across a very strange issue with Azure Runbooks and UTF-8 encoding. For most IT folks, it’s no issue if everything is in English but if you work with German speaking users, chances are that you’re going to run into encoding issues with Umlaute or other special characters eventually. For those who have no idea what I’m talking about, Umlaute are special characters like “ä”, “ö” and “ü” which are used very frequently in German.
The scenario is the following: I have a very simple Azure Runbook which sends a message card to a Teams channel.
$uri = “YourWebhookUrl”
$body = @’
{
“@context”: “https://schema.org/extensions",
“@type”: “MessageCard”,
“themeColor”: “00A4EF”,
“title”: “UTF8 Test”,
“text”: “Hello Wörld!”
}
‘@
Invoke-RestMethod -uri $uri -Method Post -body $body -ContentType ‘application/json; charset=UTF-8’
As you can see, I purposely wrote “Hello Wörld” instead of “Hello World” to demonstrate this. If the runbook is run directly (meaning, there’s no child runbook involved), there’s no issue at all and the card is sent to Teams using the correct encoding.
Working With Child Runbooks
So far so good. In more complex scenarios, you might want to build modular runbooks. An example of that would be where you have some code and some parameters in one runbook (let’s call that main runbook or child runbook) and have a couple of other runbooks which call your main runbook inline. Let’s call these runner scripts because they’re only used to run the main runbook.
The advantage of such a setup is that you only need to change the code in one place if you need to update it. For example, if the URL of the webhook changes, we only need to edit the main runbook instead of all the other runbooks as well. You can also read more about that concept in this official Microsoft Learn article.
Wrong Encoding in Child Runbooks
However, I have found that somehow the encoding gets messed up and special characters are sent to Teams in the wrong format if a child runbook is called inline from another runbook which runs in front of it.
If you want to call another runbook from any runbook in the same automation account, you can just reference it by its name. All that’s needed is the following code which really just points to another script. Note that you do need to add .ps1
at the end of your runbook name.
. .\SendMessageCardMain.ps1
It doesn’t make any difference if dot sourcing is used or not. The encoding will be wrong in either case.
This isn’t just about the message which is sent to Teams through a web request. The encoding is wrong in general and thus when using Write-Output
as well.
Workaround
The trick is to store the special characters inside a variable which is already known to the runner script (the one which will be submitted to the worker), and then calls the child runbook.
$externalText = “Wörld”
. .\SendMessageCardMain.ps1
The word which contains the special character is replaced by the variable in the main script (child runbook).
$uri = “YourWebhookUrl”
$body = @”
{
“@context”: “https://schema.org/extensions",
“@type”: “MessageCard”,
“themeColor”: “00A4EF”,
“title”: “UTF8 Test”,
“text”: “Hello $externalText!”
}
“@
Invoke-RestMethod -uri $uri -Method Post -body $body -ContentType ‘application/json; charset=UTF-8’
This way, the first runbook already knows the correct encoding and it works, just like it did in the first example where we only had one runbook.
Let’s assume that we have one main runbook which will just send Message Cards to Teams, but we also have many different runner scripts which will send different kind of messages to Teams.
This would make the code quite hard to maintain. Imagine if we want to replace the word “Wörld” with “Zürich” for example. We’d have to do this for each runner script if the variable is stored inside each runner script.
Optimized Workaround 1 (Using Automation Variables)
Instead, we can just put the code into an Automation Variable as a string. Automation Variables are saved inside the Automation Account but outside of all the runbooks. This effectively gives us a location to store the code once but all runbooks inside that Automation Account will be able to access it.
In the runner script, we import the Automation Variable using the internal Cmdlet. This is only available in Azure Runbooks and does not require additional authentication.
$AutomationVariableCode = Get-AutomationVariable -Name “AutomationVariableCode” | Out-String
Invoke-Expression $AutomationVariableCode
. .\SendMessageCardMain.ps1
There’s no mention of $externalText
inside the runbook but it’s set by Invoke-Expression
.
This allows us to change the value of the variable without touching any of our runner scripts which makes it a lot more scalable and easier to maintain while keeping the correct encoding.
Of course, the Automation Variable could also contain more complex code like a switch statement to define different messages or contain the same message in different language. For demonstration purposes, I kept it simple by just using a single value variable.
On the downside, this makes editing the code complicated and error prone, since it’s just a string stored inside a variable without any kind of syntax checking. To tackle that issue, one would need to copy it to a local IDE (e.g. VS Code) each time the code is updated and paste it back into the Automation Variable once it’s done.
Optimized Workaround 2 (Using PowerShell Runbooks)
What about storing the code in yet another runbook? This would allow for easier editing and testing right in the browser. But is it possible…? As it turns out, it is!
To be able to get the contents/code of what I call the content runbook we need to make sure that the modules Az.Accounts and Az.Automation are installed in our Automation Account.
We also need a Managed Identity to connect to Azure since we’ll be using regularAz*
Cmdlets and not internal ones this time around.
Let’s add a little more code to our runner script. My Tenant Id is also stored inside an Automation Variable, thus it’s not visible in the code.
$tenantId = Get-AutomationVariable -Name “tenantId”
$azAccount = Connect-AzAccount -Identity -TenantId $tenantId
$exportRb = Export-AzAutomationRunbook -AutomationAccountName “mzz-automation-account-001” -ResourceGroupName “mzz-rmg-001” -Name “SendMessageCardContent” -OutputFolder $env:temp
Get-Content -Path $env:temp\$exportRb -Encoding UTF8 | Out-String | Invoke-Expression
. .\SendMessageCardMain.ps1
With a Managed Identity, we don’t need to provide any kind of additional authentication. Everything is handled by the Automation Account using the Managed Identity automatically. We only need to provide Connect-AzAccount -Identity -TenantId $tenantId
.
We then export the runbook using Export-AzAutomationRunbook
to $env:temp
. Finally, we import the runbook’s content by using Get-Content
and execute its code by piping it through to Invoke-Expression
.
In case I have lost you at this point, let’s recap very briefly.
By using Invoke-Expression
instead of calling the runbook inline, we make sure that the externally stored code is running in the scope of the runner script and not the child runbook, which will keep the encoding intact.
And we’re jumping through hoops here by storing the values of the variables in another runbook so that they can be updated without touching each of our runner scripts. If it helps, you can also think about a scenario where you’re hosting some kind of monitoring or reporting solution for different customers inside your own Tenant/Automation Account. Each customer has its own runner script with their own parameters but there’s only one main runbook which contains all the code.
If we need to update the script logic, only the main runbook needs to be updated. If we need to make changes to the content of the messages, only the runbook storing these values needs to be updated.
Now let’s change the word inside the runbook to something else. Instead of editing an Automation Variable, we can just edit the runbook, which is a lot more user friendly.
Don’t forget to publish the runbook. Otherwise, the values won’t be updated. As expected, this works like a charm.
If for some reason you don’t want to read your variables into memory in the runner script and do it in the child runbook instead, you can also use the Invoke-Expression
method from there. This works as well, even if the child runbook is called inline by another runbook and the runner script doesn’t have any reference to the special character variables at all.
I have no idea why it doesn’t work if special characters are included explicitly in child runbooks though. And it took me quite some time to figure out a workaround for this. I hope that this article is useful to you, if you’ve been struggling with modular runbooks and encoding issues as well.