diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs
new file mode 100644
index 000000000..6f247f1ab
--- /dev/null
+++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs
@@ -0,0 +1,76 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure.Validation;
+
+namespace Squidex.Extensions.Actions.SignalR
+{
+ [RuleAction(
+ Title = "Azure SignalR",
+ IconImage = "",
+ IconColor = "#1566BF",
+ Display = "Send to Azure SignalR",
+ Description = "Send an message to azure SignalR.",
+ ReadMore = "https://azure.microsoft.com/fr-fr/services/signalr-service/")]
+ public sealed record SignalRAction : RuleAction
+ {
+ [LocalizedRequired]
+ [Display(Name = "Connection", Description = "The connection string to the Signal R Azure.")]
+ [Editor(RuleFieldEditor.Text)]
+ [Formattable]
+ public string ConnectionString { get; set; }
+
+ [LocalizedRequired]
+ [Display(Name = "Hub Name", Description = "The name of the hub.")]
+ [Editor(RuleFieldEditor.Text)]
+ [Formattable]
+ public string HubName { get; set; }
+
+ [LocalizedRequired]
+ [Display(Name = "Action", Description = "Broadcast = send to all User, User = send to specific user(s) specified in the 'Target' field, Group = send to specific group(s) specified in the 'Target' field")]
+ public ActionTypeEnum Action { get; set; }
+
+ [Display(Name = "Methode Name", Description = "Set the Name of the hub method received by the customer, default value 'push.")]
+ [Editor(RuleFieldEditor.Text)]
+ public string MethodName { get; set; }
+
+ [Display(Name = "Target (Optional)", Description = "Defines a user, group or target list by an id or name. For a list, define one value per line. Not necessary with the 'Broadcast' action")]
+ [Editor(RuleFieldEditor.TextArea)]
+ [Formattable]
+ public string Target { get; set; }
+
+ [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")]
+ [Editor(RuleFieldEditor.TextArea)]
+ [Formattable]
+ public string Payload { get; set; }
+
+ protected override IEnumerable CustomValidate()
+ {
+ if (!string.IsNullOrWhiteSpace(HubName) && !Regex.IsMatch(HubName, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$"))
+ {
+ yield return new ValidationError("Hub must be valid azure hub name.", nameof(HubName));
+ }
+
+ if ((Action == ActionTypeEnum.User || Action == ActionTypeEnum.Group) && string.IsNullOrWhiteSpace(Target))
+ {
+ yield return new ValidationError("Target must be specified with 'User' or 'Group' Action.", nameof(HubName));
+ }
+ }
+ }
+
+ public enum ActionTypeEnum
+ {
+ Broadcast,
+ User,
+ Group
+ }
+}
diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs
new file mode 100644
index 000000000..f4ae61ac7
--- /dev/null
+++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs
@@ -0,0 +1,119 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Azure.SignalR.Management;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.SignalR
+{
+ public sealed class SignalRActionHandler : RuleActionHandler
+ {
+ private readonly ClientPool<(string ConnectionString, string HubName), IServiceManager> clients;
+
+ public SignalRActionHandler(RuleEventFormatter formatter)
+ : base(formatter)
+ {
+ clients = new ClientPool<(string ConnectionString, string HubName), IServiceManager>(key =>
+ {
+ var serviceManager = new ServiceManagerBuilder()
+ .WithOptions(option =>
+ {
+ option.ConnectionString = key.ConnectionString;
+ option.ServiceTransportType = ServiceTransportType.Transient;
+ })
+ .Build();
+ return serviceManager;
+ });
+ }
+
+ protected override async Task<(string Description, SignalRJob Data)> CreateJobAsync(EnrichedEvent @event, SignalRAction action)
+ {
+ var hubName = await FormatAsync(action.HubName, @event);
+
+ string requestBody;
+
+ if (!string.IsNullOrWhiteSpace(action.Payload))
+ {
+ requestBody = await FormatAsync(action.Payload, @event);
+ }
+ else
+ {
+ requestBody = ToEnvelopeJson(@event);
+ }
+
+ string[] targetArray = new string[0];
+ string target = string.Empty;
+ target = await FormatAsync(action.Target, @event);
+ if (!string.IsNullOrEmpty(target))
+ {
+ targetArray = target.Split('\n');
+ }
+
+ var ruleDescription = $"Send SignalRJob to signalR hub '{hubName}'";
+
+ var ruleJob = new SignalRJob
+ {
+ ConnectionString = action.ConnectionString,
+ HubName = hubName,
+ Action = action.Action,
+ MethodName = action.MethodName,
+ Target = target,
+ TargetArray = targetArray,
+ Payload = requestBody
+ };
+ return (ruleDescription, ruleJob);
+ }
+
+ protected override async Task ExecuteJobAsync(SignalRJob job, CancellationToken ct = default)
+ {
+ var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName));
+
+ await using (var signalRContext = await signalR.CreateHubContextAsync(job.HubName))
+ {
+ var methodeName = !string.IsNullOrWhiteSpace(job.MethodName) ? job.MethodName : "push";
+
+ switch (job.Action)
+ {
+ case ActionTypeEnum.Broadcast:
+ await signalRContext.Clients.All.SendAsync(methodeName, job.Payload);
+ break;
+ case ActionTypeEnum.User:
+ await signalRContext.Clients.Users(job.TargetArray).SendAsync(methodeName, job.Payload);
+
+ break;
+ case ActionTypeEnum.Group:
+ await signalRContext.Clients.Groups(job.TargetArray).SendAsync(methodeName, job.Payload);
+
+ break;
+ }
+ }
+
+ return Result.Complete();
+ }
+ }
+
+ public sealed class SignalRJob
+ {
+ public string ConnectionString { get; set; }
+
+ public string HubName { get; set; }
+
+ public ActionTypeEnum Action { get; set; }
+
+ public string MethodName { get; set; }
+
+ public string Target { get; set; }
+
+ public string[] TargetArray { get; set; }
+
+ public string Payload { get; set; }
+ }
+}
diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs
new file mode 100644
index 000000000..e771cf13e
--- /dev/null
+++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.SignalR
+{
+ public sealed class SignalRPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
index c5531632d..9550a15fb 100644
--- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
+++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
@@ -16,6 +16,7 @@
+