We will be using MS Teams incoming webhook to POST ServiceNow Incidents assigned to a specific group into a MS Teams channel.
To make things simpler, we will create a .net core console app.
One advantage of a console app is it is very lightweight and you can take advantage of the scheduled task or job to configure the app to run at a specific time of the day.
Here are our main steps,
- Create console app in C#.
- Create necessary classes for messages.
- Create class for notification.
Let us get started...
Create console app in C#
- Start Visual Studio 2019 and Click on Create a new project
- Pick Console Application that can run on .NET Core using C#
- Configure your new project. Give it a good name.
- Pick your target framework. We will pick .net 5.0 for this example
We will start working on our classes. We will create classes for serialization/deserialization logic for the results coming back from ServiceNow API call. The de-serialized object will used for our adaptive card that will eventually be posted to a Microsoft Teams channel. This will all be in our Messages.cs file.
We will be using TableAPIs in ServiceNow, specifically incidents for this example. Basic authentication will be connecting to ServiceNow APIs. Please make sure you have an account with enough rights that you can use for this connectivity.
We will also create a Notification class with methods to retrieve ServiceNow incidents assigned to a specific group and then push the incidents as adaptive cards to a MS Teams channel. We will have this all in our Notify method which will be called from our entry point method (Main) in program.cs.
Please note:
I have made the following changes inside the entry point Main method in Program.cs file.
- Added logic to configure appsettings.json to store our configuration entries.
- Never store any critical or sensitive information exposed in configuration file. For this example, I have stored the configuration entries, including the bearer token required for ServiceNow authentication. It is best to encrypt and store any sensitive information in env files or use key vault services.
-
-
-
-
-
- private static void SetupConfiguration()
- {
- var builder = new ConfigurationBuilder()
- .AddJsonFile($"appsettings.json", true, true);
-
- var config = builder.Build();
- _config = config;
- }
- Added logic in our entry point Main method to call into SetupConfiguration() method. Also used Task.Wait() call to wait for our async Notify call to complete execution. Please note that I am also passing in _config object to Notification class to read the configuration object from our Notification class.
- private static IConfigurationRoot _config;
- static void Main(string[] args)
- {
- SetupConfiguration();
- u
- Console.WriteLine("Hello! Here are the active incidents for our team.");
- Notification notification = new(_config);
- Task t = notification.Notify();
- t.Wait();
- }
Notification (Notification.cs):
- private readonly IConfigurationRoot _config;
- public Notification(IConfigurationRoot config)
- {
- _config = config;
- }
Create necessary classes for messages
We will add all our classes necessary for our deserialization and adaptive cards inside our Messages.cs file.
Messages.cs
-
-
-
- class TeamsMessage
- {
- [JsonProperty("type")]
- public string Type { get; set; }
-
- [JsonProperty("themeColor")]
- public string ThemeColor { get; set; }
-
- [JsonProperty("summary")]
- public string Summary { get; set; }
-
- [JsonProperty("sections")]
- public TeamsMessageSections[] Sections { get; set; }
- }
-
-
-
-
- public class TeamsMessageSections
- {
- [JsonProperty("activityTitle")]
- public string ActivityTitle { get; set; }
-
- [JsonProperty("activitySubtitle")]
- public string ActivitySubtitle { get; set; }
-
- [JsonProperty("activityImage")]
- public string ActivityImage { get; set; }
-
- [JsonProperty("markdown")]
- public bool Markdown { get; set; }
- }
-
-
-
-
- public class ServiceNowReturn
- {
- [JsonProperty("result")]
- public ServiceNowReturnMessage[] Result { get; set; }
- }
-
-
-
-
- public class ServiceNowReturnMessage
- {
- [JsonProperty("number")]
- public string Number { get; set; }
-
- [JsonProperty("u_best_contact_email")]
- public string ContactEmail { get; set; }
-
- [JsonProperty("sys_id")]
- public string SysID { get; set; }
-
- [JsonProperty("short_description")]
- public string ShortDescription { get; set; }
-
- [JsonProperty("description")]
- public string Description { get; set; }
- }
TeamsMessage and TeamsMessagesSections are for our adaptive cards. We will only use a subset of data retrieved from our ServiceNow API call on our card.
ServiceNowReturn and ServiceNowReturnMessage class objects will be used for deserializing results coming from ServiceNow API calls.
Notification.cs
Notify() : method will makes calls into GetServiceNowIncidents() and PostToTeamsChannel() methods.
-
-
-
-
- public async Task<bool> Notify()
- {
- var snowResults = await GetServiceNowIncidents();
- var posted = await PostToTeamsChannel(snowResults);
- return posted;
- }
GetServiceNowIncidents(): method will use ServiceNow Table API to retrieve incidents and deserialize the results to our ServiceNowReturn class object. We will use basic authentication for connecting to ServiceNow Table APIs for this example.
-
-
-
-
- private async Task<ServiceNowReturn> GetServiceNowIncidents()
- {
- ServiceNowReturn snowReturn = null;
- var sNowIncidentsTableAPI = _config["SNowInstance"];
- using (var client = new HttpClient())
- {
- client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", _config["SNowBearerToken"]);
- var response = await client.GetAsync(sNowIncidentsTableAPI);
- var contents = await response.Content.ReadAsStringAsync();
- snowReturn = JsonConvert.DeserializeObject<ServiceNowReturn>(contents);
- }
- return snowReturn;
- }
https://<SNowInstance>.service-now.com/api/now/table/incident?sysparm_query=assignment_group=<assignmentgroup>^stateNOT IN6,7
The entries for ServiceNow API end point are in configuration file Please replace SNowInstance with your ServiceNow instance and assignmentgroup to the workgroup id of the group in ServiceNow. The assignment group id can be retrieved from ServiceNow portal. Please note that we have applied the filters to retrieve only active incidents (stateNOTIN6,7)
PostToTeamsChannel(ServiceNowReturn snowResults): method will parse ServiceNow return object array and populate TeamsMessage object required for the adaptive card and then POST into MS Teams channel.
-
-
-
-
-
- private async Task<bool> PostToTeamsChannel(ServiceNowReturn snowResults)
- {
- bool success = false;
- using (var client = new HttpClient())
- {
- foreach (var snowIncident in snowResults.Result)
- {
- TeamsMessage teamsMessage = new();
- teamsMessage.Type = "MessageCard";
- teamsMessage.ThemeColor = "0076D7";
- teamsMessage.Summary = snowIncident.Number;
- TeamsMessageSections sections = new()
- {
- ActivityTitle = string.Format("{0} - {1}", snowIncident.Number, snowIncident.ShortDescription),
- ActivitySubtitle = string.Format("{2} contact email: {1}, [View]({3}{0}) ", snowIncident.SysID, snowIncident.ContactEmail, snowIncident.Description, _config["SNowIncidentLink"]),
- ActivityImage = _config["AdaptiveCardImageURL"],
- Markdown = true
- };
- teamsMessage.Sections = new TeamsMessageSections[] { sections };
-
- var teamsPayload = JsonConvert.SerializeObject(teamsMessage);
-
- var response = await client.PostAsync(
- _config["TeamsWebhookURL"],
- new StringContent(teamsPayload, Encoding.UTF8, "application/json"));
-
- var contents = await response.Content.ReadAsStringAsync();
-
- if (response.IsSuccessStatusCode)
- {
- success = true;
- }
- }
- }
- return success;
- }
If you have not configured an incoming webhook in Microsoft Teams Channel, please use the instructions below to configure one.
Configure an incoming webhook in Microsoft Teams Channel
Right the channel of interest in MS Teams and choose Connectors.
Configure Incoming Webhook
Provide a name for the Webhook and if you prefer, upload a custom image and then click
Create. Please copy the Webhook URL and update appsettings.json TeamsWebhookURL key value to this URL.
Please update appsettings.json key values. Please note, SNowIncidentLink value is provided as a link in the card that deep links into ServiceNow incident if user prefers to get a detailed view of the incident.
"SNowInstance": "https://<SNowInstance>.service-now.com/api/now/table/incident?sysparm_query=assignment_group=<assignmentgroup>^stateNOT IN6,7",
"SNowIncidentLink": "https://<SNowInstance>.service-now.com/nav_to.do?uri=incident.do?sys_id=",
"SNowBearerToken": "<Bearer Token>",
"TeamsWebhookURL": "<TeamsIncomingWebhookUrl>",
"AdaptiveCardImageURL": "<link to a pretty image for displaying in adaptive card>"
That is it. If you have the configuration file updated with relevant values for your ServiceNow instance, Bearer token, Teams webhook, a good image link to show up in the card, you can go ahead with the build and run process. You should see the cards getting posted to the channel, if you have active incidents for your group in ServiceNow.
Here's a sample card posted to my teams channel. Please note, I have wiped out some entries, just so you don't get to see the incident descriptions :).
That is it. Happy Coding!