Triggering Webhooks from custom events
In my previous post I introduced the Kin and Carta Webhooks add-on and showed how it can be used to fire webhooks based on one of 5 out-of-the-box content events. But what if you want to trigger a webhook based on something else? While those 5 events cover the more common scenarios, there are plenty of situations which they don’t cover so in this post I’m going to show how you can register your own event types which can be used to set up webhooks in the add-on UI.
The basics
Setting up your own events is pretty straightforward and follows the same process regardless of what triggers your event. The basic steps are:
- Create an initializable module with a module dependency of "KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks"
- Create a new EventType for your event (in the "KinAndCarta.Connect.Webhooks.Models" namespace)
- Register your event type by calling the "RegisterEventType" method of IWebhookRepository
- Add a translation to give your event a friendly name
- When your event occurs, trigger the webhooks using the "TriggerWebhooks" extension method
While the steps above may tell you what you need to do to get up and running with custom events, I always find it easier to look at an example rather than a list and, in this instance, instead of coming out with some examples myself, I thought I’d take inspiration from the comments on my previous post.
Creating a “First Publish” event
In response to the previous article, Stein-Viggo Grenersen mentioned using the add-on to post to social media when new articles are published. That scenario is a great example of how the add-on can be used and it’s really simple to set up using integration platforms like IFTTT, Zapier and Azure Logic Apps but, using the OOTB content publish event, there’s a risk that you’d post to social media every time the content is published in any language, even if you’re just fixing a typo. That could mean that you’re posting the same article again and again. That may work in a demo scenario but, in the real world you probably just want to post it once, the first time you publish or, for multilingual scenarios, maybe once per language. So let’s take a look at how custom events can help.
For this example, let’s create an EventType called “First publish in language” which will only fire the first time an article is published in a given language. An EventType in this context just requires a key (to identify the event) and a flag to indicate whether the event that has occurred impacts the descendants of the content item which triggered the event (for example, the Delete event sets ImpactsDescendants to true as all descendants will be deleted too). To trigger our event, we’ll need to hook in to the publishing and published events from IContentEvents. In the publishing event, we’ll check whether any published versions exist in the current language and pass that information through to the published event using the ContentEventArgs.Items Dictionary which is shared between events triggered by the same action. If no existing published versions exist, we then trigger our webhook from within the “published” event. To trigger the webhooks attached to a content item, we use the aptly named "TriggerWebhooks" extension method (from KinAndCarta.Connect.Webhooks.Extensions) and pass in our event object, the IWebhookRepository instance and any additional data we want to include about our event which, in this case, is null.
The code looks like this…
[InitializableModule]
[ModuleDependency(typeof(KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks))]
public class FirstPublishWebhookEventsInitialisation : IInitializableModule
{
private IWebhookRepository _webhookRepo;
private IContentLoader _contentLoader;
private IContentEvents _contentEvents;
private EventType _customEvent;
public void Initialize(InitializationEngine context)
{
//Get dependencies
_contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
_webhookRepo = context.Locate.Advanced.GetInstance<IWebhookRepository>();
_contentLoader = context.Locate.Advanced.GetInstance<IContentLoader>();
//Create & register our event
_customEvent = new EventType { ImpactsDescendants = false, Key = "FIRST_PUBLISH_IN_LANGUAGE" };
_webhookRepo.RegisterEventType(_customEvent);
//Setup Event handlers
_contentEvents.PublishingContent += ContentPublishing;
_contentEvents.PublishedContent += ContentPublished;
}
private void ContentPublishing(object sender, ContentEventArgs e)
{
var versionRepo = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
bool firstPublish;
//Check whether a published version already exists
//If content can be translated, check if a published version exists in the current language
if (e.Content is ILocalizable localContent)
{
firstPublish = versionRepo.LoadPublished(e.ContentLink.ToReferenceWithoutVersion(), localContent.Language.Name) == null;
}
else
{
firstPublish = versionRepo.LoadPublished(e.ContentLink.ToReferenceWithoutVersion()) == null;
}
//Set a value for the subsequent publish event to check
e.Items.Add("FirstPublish", firstPublish);
}
private void ContentPublished(object sender, ContentEventArgs e)
{
//Check for our FirstPublish valie
if (e.Items["FirstPublish"] as bool? ?? false)
{
//TriggerWebhooks extension uses KinAndCarta.Connect.Webhooks.Extensions namespace
e.Content.TriggerWebhooks(_customEvent, _webhookRepo, null);
}
}
public void Uninitialize(InitializationEngine context)
{
_contentEvents.PublishingContent -= ContentPublishing;
_contentEvents.PublishedContent -= ContentPublished;
}
}
If we compile and run our code then look at a webhook in the Webhooks UI, here’s what we’ll see.
It’s functional, but not the most user friendly so let’s set up a translation for our event name. Translations of the event names are handled like any other translation in the CMS as you can see in the XML snippet below.
<?xml version="1.0" encoding="utf-8" ?>
<languages>
<language name="English" id="en">
<Webhooks>
<EventTypes>
<PUBLISH>Content Published</PUBLISH>
<MOVING>Content Moving</MOVING>
<MOVE>Content Moved</MOVE>
<RECYCLE>Content Sent to Recycle Bin</RECYCLE>
<DELETE>Content Deleted from Recycle Bin</DELETE>
<FIRST_PUBLISH_IN_LANGUAGE>First publish in language</FIRST_PUBLISH_IN_LANGUAGE>
</EventTypes>
</Webhooks>
</language>
</languages>
Reloading the screen, we can now see our friendly event name in the list of events and use it to fire a webhook the first time a content item is published in a given language.
Reporting on Scheduled Job execution
In another comment on the original article, Jay Wilkinson suggested using webhooks to act on the outcome of a scheduled job execution, for example, sending an email if a job fails. As you’ve probably realised, we’re not restricted to just firing webhooks on content events. We can trigger webhooks from anywhere in our code, in response to any action, however, as seen in the example above, we need to trigger the webhook using a content item which will be passed in the payload of the webhook. In this particular example though, we’re not interested in the content so we’ll trigger the webhook using the global block folder, knowing it’s always there, easily referenced and contains minimal data of its own (it's not ILocalizable so won't contain language information unlike the content root node). We are, however, interested in information about the scheduled job execution and we can add that data to the webhook payload pretty easily using the “extraData” parameter when we call TriggerWebhooks. The extraData parameter is a Dictionary<string, object> so we can easily add pretty much any data we need.
In this example we’re going to hook into the Executed event exposed by IScheduledJobEvents which fires when a scheduled job has finished executing. The basics are the same as the example above but this time we’re going to extract some information on the job which has executed and pass it into the webhook payload via the extraData parameter. Putting it all together, here’s how it looks.
[InitializableModule]
[ModuleDependency(typeof(KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks))]
public class ScheduledJobCompletionWebhookInitialisation : IInitializableModule
{
private IScheduledJobEvents _scheduledJobEvents;
private IWebhookRepository _webhookRepo;
private IContentLoader _contentLoader;
private EventType _customEvent;
public void Initialize(InitializationEngine context)
{
//Get dependencies
_webhookRepo = context.Locate.Advanced.GetInstance<IWebhookRepository>();
_contentLoader = context.Locate.Advanced.GetInstance<IContentLoader>();
_scheduledJobEvents = context.Locate.Advanced.GetInstance<IScheduledJobEvents>();
//Create & register our event
_customEvent = new EventType { ImpactsDescendants = false, Key = "SCHEDULED_JOB_EXECUTED" };
_webhookRepo.RegisterEventType(_customEvent);
//Attach to scheduled job executed event
_scheduledJobEvents.Executed += executedScheduledJob;
}
private void executedScheduledJob(object sender, ScheduledJobEventArgs e)
{
//Build extra info to pass in payload
var jobInfo = new Dictionary<string, object>();
jobInfo.Add("Name", e.Job.Name);
jobInfo.Add("Success", !e.Job.HasLastExecutionFailed);
jobInfo.Add("Message", e.Job.LastExecutionMessage);
//Trigger webhook on asset root page
_contentLoader.Get<IContent>(ContentReference.GlobalBlockFolder).TriggerWebhooks(_customEvent, _webhookRepo, jobInfo);
}
public void Uninitialize(InitializationEngine context)
{
//Clean up event
_scheduledJobEvents.Executed -= executedScheduledJob;
}
}
As before, you’ll probably want to add a translation to give the event a friendly name.
Once you’ve compiled and run the solution you can set up a webhook which fires on scheduled job executions, just remember to select the root node or the global blocks folder when setting up the webhook as that’s the node we’re attaching our custom event to. The payload data will look something like this.
{
"ReferencedBy": [],
"Content": {
"ContentLink": {
"Id": 3,
"WorkId": 0,
"GuidValue": "e56f85d0-e833-4e02-976a-2d11fe4d598c",
"ProviderName": null,
"Url": null,
"Expanded": null
},
"Name": "For All Sites",
"Language": null,
"ExistingLanguages": [],
"MasterLanguage": null,
"ContentType": [
"Folder",
"SysContentFolder"
],
"ParentLink": {
"Id": 1,
"WorkId": 0,
"GuidValue": "43f936c9-9b23-4ea3-97b2-61c538ad07c9",
"ProviderName": null,
"Url": null,
"Expanded": null
},
"RouteSegment": "SysGlobalAssets",
"Url": null,
"Changed": "1999-01-01T00:00:00Z",
"Created": "1999-01-01T00:00:00Z",
"StartPublish": null,
"StopPublish": null,
"Saved": "1999-01-01T00:00:00Z",
"Status": null
},
"ExtraData": {
"Name": "Publish Delayed Content Versions",
"Success": true,
"Message": "Nothing was published."
},
"ContentInfo": {
"ContentId": 3,
"ContentGuid": "e56f85d0-e833-4e02-976a-2d11fe4d598c",
"ContentLanguage": "",
"Name": "For All Sites",
"Url": null
},
"EventType": "SCHEDULED_JOB_EXECUTED",
"EventTime": "2021-06-10T10:00:10.7754622Z"
}
We can then easily set up a receiver in, for example, Azure Logic Apps to receive our webhook and use the data passed in the extraData parameter to perform actions such as sending a Slack message if a specific job fails.
So there we have it - just a couple of simple examples to show how you can use Webhooks with custom events but hopefully it’s conveyed how simple and flexible it is to set up your own event types. I look forward to hearing how you put it to use in your own projects.
As I mentioned in the previous post, the add-on is available in the Episerver NuGet feed under “KinAndCarta.Webhooks” and requires no particular set-up other than installing the package. Full source code is also available on GitHub under an MIT license.
Nice one Paul, and thanks for providing an example on the Scheduled Jobs stuff, that's really useful.
Nice one Paul, and thanks for providing an example on the Scheduled Jobs stuff, that's really useful.