diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 465355394d67..c214e71a35de 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -46,6 +46,7 @@
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 09bd6dc331b6..7fc1a685b001 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -432,6 +432,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sk-chatgpt-azure-function",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{78785CB1-66CF-4895-D7E5-A440DD84BE86}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents", "samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.csproj", "{065E7F63-3475-4EEE-93EE-D2A4BF7AA538}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1159,6 +1161,12 @@ Global
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Publish|Any CPU.Build.0 = Debug|Any CPU
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Release|Any CPU.Build.0 = Release|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1318,6 +1326,7 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{2EB6E4C2-606D-B638-2E08-49EA2061C428} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{78785CB1-66CF-4895-D7E5-A440DD84BE86} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {065E7F63-3475-4EEE-93EE-D2A4BF7AA538} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs
new file mode 100644
index 000000000000..d9d980ce5075
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+internal sealed class AppConfig
+{
+ ///
+ /// The configuration for the Azure EntraId authentication.
+ ///
+ public AzureEntraIdConfig? AzureEntraId { get; set; }
+
+ ///
+ /// Ensures that the configuration is valid.
+ ///
+ internal void Validate()
+ {
+ ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientId, nameof(this.AzureEntraId.ClientId));
+ ArgumentNullException.ThrowIfNull(this.AzureEntraId?.TenantId, nameof(this.AzureEntraId.TenantId));
+
+ if (this.AzureEntraId.InteractiveBrowserAuthentication)
+ {
+ ArgumentNullException.ThrowIfNull(this.AzureEntraId.InteractiveBrowserRedirectUri, nameof(this.AzureEntraId.InteractiveBrowserRedirectUri));
+ }
+ else
+ {
+ ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientSecret, nameof(this.AzureEntraId.ClientSecret));
+ }
+ }
+
+ internal sealed class AzureEntraIdConfig
+ {
+ ///
+ /// App Registration Client Id
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// App Registration Tenant Id
+ ///
+ public string? TenantId { get; set; }
+
+ ///
+ /// The client secret to use for the Azure EntraId authentication.
+ ///
+ ///
+ /// This is required if InteractiveBrowserAuthentication is false. (App Authentication)
+ ///
+ public string? ClientSecret { get; set; }
+
+ ///
+ /// Specifies whether to use interactive browser authentication (Delegated User Authentication) or App authentication.
+ ///
+ public bool InteractiveBrowserAuthentication { get; set; }
+
+ ///
+ /// When using interactive browser authentication, the redirect URI to use.
+ ///
+ public string? InteractiveBrowserRedirectUri { get; set; } = "http://localhost";
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs
new file mode 100644
index 000000000000..98ef47862db0
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs
@@ -0,0 +1,135 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Graph;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Process.Models;
+using ProcessWithCloudEvents.Processes;
+using ProcessWithCloudEvents.Processes.Steps;
+
+namespace ProcessWithCloudEvents.Controllers;
+///
+/// Base class that contains common methods to be used when using SK Processes and Counter common api entrypoints
+///
+public abstract class CounterBaseController : ControllerBase
+{
+ ///
+ /// Kernel to be used to run the SK Process
+ ///
+ internal Kernel Kernel { get; init; }
+
+ ///
+ /// SK Process to be used to hold the counter logic
+ ///
+ internal KernelProcess Process { get; init; }
+
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+ };
+
+ internal Kernel BuildKernel(GraphServiceClient? graphClient = null)
+ {
+ var builder = Kernel.CreateBuilder();
+ if (graphClient != null)
+ {
+ builder.Services.AddSingleton(graphClient);
+ }
+ return builder.Build();
+ }
+
+ internal KernelProcess InitializeProcess(ProcessBuilder process)
+ {
+ this.InitializeStateFile(process.Name);
+ var processState = this.LoadProcessState(process.Name);
+ return process.Build(processState);
+ }
+
+ private string GetTemporaryProcessFilePath(string processName)
+ {
+ return Path.Combine(Path.GetTempPath(), $"{processName}.json");
+ }
+
+ internal void InitializeStateFile(string processName)
+ {
+ // Initialize the path for the temporary file
+ var tempProcessFile = this.GetTemporaryProcessFilePath(processName);
+
+ // If the file does not exist, create it and initialize with zero
+ if (!System.IO.File.Exists(tempProcessFile))
+ {
+ System.IO.File.WriteAllText(tempProcessFile, "");
+ }
+ }
+
+ internal void SaveProcessState(string processName, KernelProcessStateMetadata processStateInfo)
+ {
+ var content = JsonSerializer.Serialize(processStateInfo, s_jsonOptions);
+ System.IO.File.WriteAllText(this.GetTemporaryProcessFilePath(processName), content);
+ }
+
+ internal KernelProcessStateMetadata? LoadProcessState(string processName)
+ {
+ try
+ {
+ using StreamReader reader = new(this.GetTemporaryProcessFilePath(processName));
+ var content = reader.ReadToEnd();
+ return JsonSerializer.Deserialize(content, s_jsonOptions);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ internal void StoreProcessState(KernelProcess process)
+ {
+ var stateMetadata = process.ToProcessStateMetadata();
+ this.SaveProcessState(process.State.Name, stateMetadata);
+ }
+
+ internal KernelProcessStepState? GetCounterState(KernelProcess process)
+ {
+ // TODO: Replace when there is a better way of extracting snapshot of local state
+ return process.Steps
+ .First(step => step.State.Name == RequestCounterProcess.StepNames.Counter).State as KernelProcessStepState;
+ }
+
+ internal async Task StartProcessWithEventAsync(string eventName, object? eventData = null)
+ {
+ var runningProcess = await this.Process.StartAsync(this.Kernel, new() { Id = eventName, Data = eventData });
+ var processState = await runningProcess.GetStateAsync();
+ this.StoreProcessState(processState);
+
+ return processState;
+ }
+
+ ///
+ /// API entry point to increase the counter
+ ///
+ /// current counter value
+ public virtual async Task IncreaseCounterAsync()
+ {
+ return await Task.FromResult(0);
+ }
+
+ ///
+ /// API entry point to decrease the counter
+ ///
+ /// current counter value
+ public virtual async Task DecreaseCounterAsync()
+ {
+ return await Task.FromResult(0);
+ }
+
+ ///
+ /// API entry point to reset counter value to 0
+ ///
+ /// current counter value
+ public virtual async Task ResetCounterAsync()
+ {
+ return await Task.FromResult(0);
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs
new file mode 100644
index 000000000000..9b069851180e
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Graph;
+using ProcessWithCloudEvents.Processes;
+
+namespace ProcessWithCloudEvents.Controllers;
+[ApiController]
+[Route("[controller]")]
+public class CounterWithCloudStepsController : CounterBaseController
+{
+ private readonly ILogger _logger;
+
+ public CounterWithCloudStepsController(ILogger logger, GraphServiceClient graphClient)
+ {
+ this._logger = logger;
+
+ this.Kernel = this.BuildKernel(graphClient);
+ this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithCloudSteps());
+ }
+
+ ///
+ [HttpGet("increase", Name = "IncreaseWithCloudSteps")]
+ public override async Task IncreaseCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+
+ ///
+ [HttpGet("decrease", Name = "DecreaseWithCloudSteps")]
+ public override async Task DecreaseCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+
+ ///
+ [HttpGet("reset", Name = "ResetCounterWithCloudSteps")]
+ public override async Task ResetCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs
new file mode 100644
index 000000000000..bc57705e0a34
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Graph;
+using ProcessWithCloudEvents.Processes;
+
+namespace ProcessWithCloudEvents.Controllers;
+[ApiController]
+[Route("[controller]")]
+public class CounterWithCloudSubscribersController : CounterBaseController
+{
+ private readonly ILogger _logger;
+
+ public CounterWithCloudSubscribersController(ILogger logger, GraphServiceClient graphClient)
+ {
+ this._logger = logger;
+ this.Kernel = this.BuildKernel();
+
+ var serviceProvider = new ServiceCollection()
+ .AddSingleton(graphClient)
+ .BuildServiceProvider();
+ this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithProcessSubscriber(serviceProvider));
+ }
+
+ ///
+ [HttpGet("increase", Name = "IncreaseCounterWithCloudSubscribers")]
+ public override async Task IncreaseCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+
+ ///
+ [HttpGet("decrease", Name = "DecreaseCounterWithCloudSubscribers")]
+ public override async Task DecreaseCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+
+ ///
+ [HttpGet("reset", Name = "ResetCounterWithCloudSubscribers")]
+ public override async Task ResetCounterAsync()
+ {
+ var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest);
+ var runningProcess = await this.StartProcessWithEventAsync(eventName);
+ var counterState = this.GetCounterState(runningProcess);
+
+ return counterState?.State?.Counter ?? -1;
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs
new file mode 100644
index 000000000000..470352b928b6
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.Core;
+using Azure.Identity;
+using Microsoft.Graph;
+
+public static class GraphServiceProvider
+{
+ public static GraphServiceClient CreateGraphService()
+ {
+ string[] scopes;
+
+ var config = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory()) // Set the base path for appsettings.json
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // Load appsettings.json
+ .AddUserSecrets()
+ .AddEnvironmentVariables()
+ .Build()
+ .Get() ??
+ throw new InvalidOperationException("Configuration is not setup correctly.");
+
+ config.Validate();
+
+ TokenCredential credential = null!;
+ if (config.AzureEntraId!.InteractiveBrowserAuthentication) // Authentication As User
+ {
+ /// Use this if using user delegated permissions
+ scopes = ["User.Read", "Mail.Send"];
+
+ credential = new InteractiveBrowserCredential(
+ new InteractiveBrowserCredentialOptions
+ {
+ TenantId = config.AzureEntraId.TenantId,
+ ClientId = config.AzureEntraId.ClientId,
+ AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
+ RedirectUri = new Uri(config.AzureEntraId.InteractiveBrowserRedirectUri!)
+ });
+ }
+ else // Authentication As Application
+ {
+ scopes = ["https://graph.microsoft.com/.default"];
+
+ credential = new ClientSecretCredential(
+ config.AzureEntraId.TenantId,
+ config.AzureEntraId.ClientId,
+ config.AzureEntraId.ClientSecret);
+ }
+
+ return new GraphServiceClient(credential, scopes);
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs
new file mode 100644
index 000000000000..f7253d3e2833
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Graph.Me.SendMail;
+using Microsoft.Graph.Models;
+
+namespace ProcessWithCloudEvents.MicrosoftGraph;
+
+///
+/// Factory that creates Microsoft Graph related objects
+///
+public static class GraphRequestFactory
+{
+ ///
+ /// Method that creates MailPost Body with defined subject, content and recipients
+ ///
+ /// subject of the email
+ /// content of the email
+ /// recipients of the email
+ ///
+ public static SendMailPostRequestBody CreateEmailBody(string subject, string content, List recipients)
+ {
+ var message = new SendMailPostRequestBody()
+ {
+ Message = new Microsoft.Graph.Models.Message()
+ {
+ Subject = subject,
+ Body = new()
+ {
+ ContentType = Microsoft.Graph.Models.BodyType.Text,
+ Content = content,
+ },
+ ToRecipients = recipients.Select(address => new Recipient { EmailAddress = new() { Address = address } }).ToList(),
+ },
+ SaveToSentItems = true,
+ };
+
+ return message;
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj
new file mode 100644
index 000000000000..0d8d4711ef5b
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ $(NoWarn);CA2007,CA1861,CA1050,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110
+
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http
new file mode 100644
index 000000000000..7111d4bcb145
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http
@@ -0,0 +1,24 @@
+@ProcessWithCloudEvents_HostAddress = http://localhost:5077
+
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/increase
+Accept: application/json
+
+###
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/decrease
+Accept: application/json
+
+###
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/reset
+Accept: application/json
+
+###
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/increase
+Accept: application/json
+
+###
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/decrease
+Accept: application/json
+
+###
+GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/reset
+Accept: application/json
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs
new file mode 100644
index 000000000000..edef808000bb
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Graph;
+using Microsoft.Graph.Me.SendMail;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Process;
+using ProcessWithCloudEvents.MicrosoftGraph;
+using ProcessWithCloudEvents.Processes.Steps;
+
+namespace ProcessWithCloudEvents.Processes;
+
+public static class RequestCounterProcess
+{
+ public static class StepNames
+ {
+ public const string Counter = nameof(Counter);
+ public const string CounterInterceptor = nameof(CounterInterceptor);
+ public const string SendEmail = nameof(SendEmail);
+ }
+
+ public enum CounterProcessEvents
+ {
+ IncreaseCounterRequest,
+ DecreaseCounterRequest,
+ ResetCounterRequest,
+ OnCounterReset,
+ OnCounterResult
+ }
+
+ public static string GetEventName(CounterProcessEvents processEvent)
+ {
+ return Enum.GetName(processEvent) ?? "";
+ }
+
+ public static ProcessBuilder CreateProcessWithCloudSteps()
+ {
+ var processBuilder = new ProcessBuilder("RequestCounterProcess");
+
+ var counterStep = processBuilder.AddStepFromType(StepNames.Counter);
+ var counterInterceptorStep = processBuilder.AddStepFromType(StepNames.CounterInterceptor);
+ var emailSenderStep = processBuilder.AddStepFromType(StepNames.SendEmail);
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.IncreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.DecreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.ResetCounter));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.IncreaseCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.DecreaseCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.ResetCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.StepFunctions.SendCounterResetEmail));
+
+ counterInterceptorStep
+ .OnFunctionResult(CounterInterceptorStep.StepFunctions.InterceptCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.StepFunctions.SendCounterChangeEmail));
+
+ return processBuilder;
+ }
+
+ public static ProcessBuilder CreateProcessWithProcessSubscriber(IServiceProvider serviceProvider)
+ {
+ var processBuilder = new ProcessBuilder("CounterWithProcessSubscriber");
+
+ var counterStep = processBuilder.AddStepFromType(StepNames.Counter);
+ var counterInterceptorStep = processBuilder.AddStepFromType(StepNames.CounterInterceptor);
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.IncreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.DecreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.ResetCounter));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.IncreaseCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.DecreaseCounter)
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterStep
+ .OnFunctionResult(CounterStep.StepFunctions.ResetCounter)
+ .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterReset))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterInterceptorStep
+ .OnFunctionResult(CounterInterceptorStep.StepFunctions.InterceptCounter)
+ .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterResult));
+
+ processBuilder.LinkEventSubscribersFromType(serviceProvider);
+
+ return processBuilder;
+ }
+
+ public class CounterProcessSubscriber : KernelProcessEventsSubscriber
+ {
+ private SendMailPostRequestBody GenerateEmailRequest(int counter, string emailAddress, string subject)
+ {
+ var message = GraphRequestFactory.CreateEmailBody(
+ subject: $"{subject} - using SK event subscribers",
+ content: $"The counter is {counter}",
+ recipients: [emailAddress]);
+
+ return message;
+ }
+
+ [ProcessEventSubscriber(CounterProcessEvents.OnCounterResult)]
+ public async Task OnCounterResultReceivedAsync(int? counterResult)
+ {
+ if (!counterResult.HasValue)
+ {
+ return;
+ }
+
+ try
+ {
+ var graphClient = this.ServiceProvider?.GetRequiredService();
+ var user = await graphClient?.Me.GetAsync();
+ var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has changed");
+ await graphClient?.Me.SendMail.PostAsync(graphEmailMessage);
+ }
+ catch (Exception e)
+ {
+ throw new KernelException($"Something went wrong and couldn't send email - {e}");
+ }
+ }
+
+ [ProcessEventSubscriber(CounterProcessEvents.OnCounterReset)]
+ public async Task OnCounterResetReceivedAsync(int? counterResult)
+ {
+ if (!counterResult.HasValue)
+ {
+ return;
+ }
+
+ try
+ {
+ var graphClient = this.ServiceProvider?.GetRequiredService();
+ var user = await graphClient.Me.GetAsync();
+ var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has been reset");
+ await graphClient?.Me.SendMail.PostAsync(graphEmailMessage);
+ }
+ catch (Exception e)
+ {
+ throw new KernelException($"Something went wrong and couldn't send email - {e}");
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs
new file mode 100644
index 000000000000..827b346f9232
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+
+namespace ProcessWithCloudEvents.Processes.Steps;
+
+public class CounterInterceptorStep : KernelProcessStep
+{
+ public static class StepFunctions
+ {
+ public const string InterceptCounter = nameof(InterceptCounter);
+ }
+
+ [KernelFunction(StepFunctions.InterceptCounter)]
+ public int? InterceptCounter(int counterStatus)
+ {
+ var multipleOf = 3;
+ if (counterStatus != 0 && counterStatus % multipleOf == 0)
+ {
+ // Only return counter if counter is a multiple of "multipleOf"
+ return counterStatus;
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs
new file mode 100644
index 000000000000..48738c0bfed4
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+
+namespace ProcessWithCloudEvents.Processes.Steps;
+
+public class CounterStep : KernelProcessStep
+{
+ public static class StepFunctions
+ {
+ public const string IncreaseCounter = nameof(IncreaseCounter);
+ public const string DecreaseCounter = nameof(DecreaseCounter);
+ public const string ResetCounter = nameof(ResetCounter);
+ }
+
+ public static class OutputEvents
+ {
+ public const string CounterResult = nameof(CounterResult);
+ }
+
+ internal CounterStepState? _state;
+
+ public override ValueTask ActivateAsync(KernelProcessStepState state)
+ {
+ this._state = state.State;
+ return ValueTask.CompletedTask;
+ }
+
+ [KernelFunction(StepFunctions.IncreaseCounter)]
+ public async Task IncreaseCounterAsync(KernelProcessStepContext context)
+ {
+ this._state!.Counter += this._state.CounterIncrements;
+
+ if (this._state!.Counter > 5)
+ {
+ await context.EmitEventAsync(OutputEvents.CounterResult, this._state.Counter);
+ }
+ this._state.LastCounterUpdate = DateTime.UtcNow;
+
+ return this._state.Counter;
+ }
+
+ [KernelFunction(StepFunctions.DecreaseCounter)]
+ public async Task DecreaseCounterAsync(KernelProcessStepContext context)
+ {
+ this._state!.Counter -= this._state.CounterIncrements;
+
+ if (this._state!.Counter > 5)
+ {
+ await context.EmitEventAsync(OutputEvents.CounterResult, this._state.Counter);
+ }
+ this._state.LastCounterUpdate = DateTime.UtcNow;
+
+ return this._state.Counter;
+ }
+
+ [KernelFunction(StepFunctions.ResetCounter)]
+ public async Task ResetCounterAsync(KernelProcessStepContext context)
+ {
+ this._state!.Counter = 0;
+ return this._state.Counter;
+ }
+}
+
+public class CounterStepState
+{
+ public int Counter { get; set; } = 0;
+ public int CounterIncrements { get; set; } = 1;
+
+ public DateTime? LastCounterUpdate { get; set; } = null;
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs
new file mode 100644
index 000000000000..92fc6244c925
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Graph;
+using Microsoft.Graph.Me.SendMail;
+using Microsoft.SemanticKernel;
+using ProcessWithCloudEvents.MicrosoftGraph;
+
+namespace ProcessWithCloudEvents.Processes.Steps;
+
+public class SendEmailStep : KernelProcessStep
+{
+ public static class OutputEvents
+ {
+ public const string SendEmailSuccess = nameof(SendEmailSuccess);
+ public const string SendEmailFailure = nameof(SendEmailFailure);
+ }
+
+ public static class StepFunctions
+ {
+ public const string SendCounterChangeEmail = nameof(SendCounterChangeEmail);
+ public const string SendCounterResetEmail = nameof(SendCounterResetEmail);
+ }
+
+ public SendEmailStep() { }
+
+ protected SendMailPostRequestBody PopulateMicrosoftGraphMailMessage(object inputData, string emailAddress, string subject)
+ {
+ var message = GraphRequestFactory.CreateEmailBody(
+ subject: $"{subject} - using SK cloud step",
+ content: $"The counter is {(int)inputData}",
+ recipients: [emailAddress]);
+
+ return message;
+ }
+
+ [KernelFunction(StepFunctions.SendCounterChangeEmail)]
+ public async Task PublishCounterChangedEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData)
+ {
+ if (inputData == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var graphClient = kernel.GetRequiredService();
+ var user = await graphClient.Me.GetAsync();
+ var graphEmailMessage = this.PopulateMicrosoftGraphMailMessage(inputData, user!.Mail!, subject: "The counter has changed");
+ await graphClient.Me.SendMail.PostAsync(graphEmailMessage).ConfigureAwait(false);
+
+ await context.EmitEventAsync(OutputEvents.SendEmailSuccess);
+ }
+ catch (Exception e)
+ {
+ await context.EmitEventAsync(OutputEvents.SendEmailFailure, e, visibility: KernelProcessEventVisibility.Public);
+ throw new KernelException($"Something went wrong and couldn't send email - {e}");
+ }
+ }
+
+ [KernelFunction(StepFunctions.SendCounterResetEmail)]
+ public async Task PublishCounterResetEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData)
+ {
+ if (inputData == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var graphClient = kernel.GetRequiredService();
+ var user = await graphClient.Me.GetAsync();
+ var graphEmailMessage = this.PopulateMicrosoftGraphMailMessage(inputData, user!.Mail!, subject: "The counter has been reset");
+ await graphClient.Me.SendMail.PostAsync(graphEmailMessage).ConfigureAwait(false);
+
+ await context.EmitEventAsync(OutputEvents.SendEmailSuccess);
+ }
+ catch (Exception e)
+ {
+ await context.EmitEventAsync(OutputEvents.SendEmailFailure, e, visibility: KernelProcessEventVisibility.Public);
+ throw new KernelException($"Something went wrong and couldn't send email - {e}");
+ }
+ }
+}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs
new file mode 100644
index 000000000000..dae96b88b210
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Graph;
+using ProcessWithCloudEvents.Controllers;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddSingleton(GraphServiceProvider.CreateGraphService());
+
+// For demo purposes making the Counter a singleton so it is not instantiated on every new request
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+// Add services to the container.
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md
new file mode 100644
index 000000000000..3d5f2983cdfe
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md
@@ -0,0 +1,251 @@
+# Process With Cloud Events Demo
+
+This demo contains an ASP.NET core API that showcases the use of cloud events using SK Processes Steps and SK Process with Event Subscribers.
+
+
+For more information about Semantic Kernel Processes, see the following documentation:
+
+## Semantic Kernel Processes
+
+- [Overview of the Process Framework (docs)](https://learn.microsoft.com/semantic-kernel/frameworks/process/process-framework)
+- [Getting Started with Processes (samples)](../../GettingStartedWithProcesses/)
+
+## Demo
+
+### Process: Counter with Cloud Events
+
+#### Steps
+
+##### Counter Step
+
+A simple counter has 3 main functionalities:
+
+- Increase count
+- Decrease count
+- Reset count (set counter to 0)
+
+To achieve this behavior the SK Stateful Step `Processes/Steps/CounterStep.cs` was created.
+On every request it stores that state that can be used to restore the state on the next request.
+
+##### Counter Interceptor Step
+
+This step works as a filter that only passes the counter value if it is a multiple of `multipleOf` else passes a null value.
+
+##### Send Email Step
+
+This step sends an email if receiving a not nullable int value to the same email used on log in.
+
+#### Processes
+
+##### Process With Cloud Steps
+
+```mermaid
+flowchart LR
+ subgraph API
+ ApiIncrease["/increase"]
+ ApiDecrease["/decrease"]
+ ApiReset["/reset"]
+ end
+
+ subgraph process[SK Process]
+ direction LR
+ subgraph counter[Counter Step]
+ increaseCounter[IncreaseCounterAsync
Function]
+ decreaseCounter[DecreaseCounterAsync
Function]
+ resetCounter[ResetCounterAsync
Function]
+ end
+
+ counterInterceptor[Counter
Interceptor
Step]
+
+ subgraph sendEmail[Send Email Step]
+ sendCounterChangedEmail[PublishCounterChangedEmailMessageAsync
Function]
+ sendResetEmail[PublishCounterResetEmailMessageAsync
Function]
+ end
+
+ increaseCounter--> counterInterceptor
+ decreaseCounter--> counterInterceptor
+
+ counterInterceptor-->sendCounterChangedEmail
+ resetCounter-->sendResetEmail
+ end
+
+ ApiIncrease<-->|IncreaseCounterRequest|increaseCounter
+ ApiDecrease<-->|DecreaseCounterRequest|decreaseCounter
+ ApiReset<-->|ResetCounterRequest|resetCounter
+```
+
+Cloud events related logic is encapsulated in a step.
+
+**Breakdown**
+
+- When building the process Kernel used in the SK Process, the cloud event client has to be passed to the Kernel.
+
+- When using `Microsoft Graph`, after completing the [Microsoft Graph Setup](./#microsoft-graph-setup), To achieve the proper setup the following is needed:
+
+ 1. The specific service (`GraphServiceClient` in this case) needs to be added to the Services that are used by the kernel of the process:
+
+ ```C#
+ internal Kernel BuildKernel(GraphServiceClient? graphClient = null)
+ {
+ var builder = Kernel.CreateBuilder();
+ if (graphClient != null)
+ {
+ builder.Services.AddSingleton(graphClient);
+ }
+ return builder.Build();
+ }
+ ```
+ 2. Since now all steps have access to the configured kernel, inside a step, it now can make use of the service by doing:
+ ```C#
+ var graphClient = kernel.GetRequiredService();
+ ```
+
+##### Process With Cloud Process Subscribers
+
+Cloud events related logic is encapsulated in SK Event Subscribers.
+
+```mermaid
+flowchart LR
+ subgraph API
+ ApiIncrease["/increase"]
+ ApiDecrease["/decrease"]
+ ApiReset["/reset"]
+ end
+
+ subgraph process[SK Process - CreateProcessWithProcessSubscriber]
+ direction TB
+ subgraph counter[Counter Step]
+ increaseCounter[IncreaseCounterAsync
Function]
+ decreaseCounter[DecreaseCounterAsync
Function]
+ resetCounter[ResetCounterAsync
Function]
+ end
+ counterInterceptor[Counter
Interceptor
Step]
+
+ increaseCounter--> counterInterceptor
+ decreaseCounter--> counterInterceptor
+ end
+
+ subgraph processInterceptor[SK Process Subscribers - CounterProcessSubscriber]
+ OnCounterResultReceivedAsync
+ OnCounterResetReceivedAsync
+ end
+
+ counterInterceptor-->|OnCounterResult|OnCounterResultReceivedAsync
+ resetCounter-->|OnCounterReset|OnCounterResetReceivedAsync
+
+ ApiIncrease<-->|IncreaseCounterRequest|increaseCounter
+ ApiDecrease<-->|DecreaseCounterRequest|decreaseCounter
+ ApiReset<-->|ResetCounterRequest|resetCounter
+```
+**Breakdown**
+
+- When building the process Kernel used in the SK Process, the cloud event client has to be passed to the Event Subscribers.
+
+- When using `Microsoft Graph`, after completing the [Microsoft Graph Setup](./#microsoft-graph-setup), the Event Subscribers can be linked by doing:
+ 1. Creating an enum that contains the process events of interest.
+ ```C#
+ public enum CounterProcessEvents
+ {
+ IncreaseCounterRequest,
+ DecreaseCounterRequest,
+ ResetCounterRequest,
+ OnCounterReset,
+ OnCounterResult
+ }
+ ```
+ 2. On the existing process, adding which events can be accessed externally using `EmitAsProcessEvent()`:
+ ```C#
+ var processBuilder = new ProcessBuilder("CounterWithProcessSubscriber");
+
+ ...
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter));
+
+ processBuilder
+ .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter));
+
+ ...
+
+ counterStep
+ .OnFunctionResult(CounterStep.Functions.ResetCounter)
+ .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterReset))
+ .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep));
+
+ counterInterceptorStep
+ .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter)
+ .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterResult));
+ ```
+ 3. Create a `KernelProcessEventsSubscriber` based class that with the `ProcessEventSubscriber` attributes to link specific process events to specific methods to execute.
+ ```C#
+ public class CounterProcessSubscriber : KernelProcessEventsSubscriber
+ {
+ [ProcessEventSubscriber(CounterProcessEvents.OnCounterResult)]
+ public async Task OnCounterResultReceivedAsync(int? counterResult)
+ {
+ if (!counterResult.HasValue)
+ {
+ return;
+ }
+
+ try
+ {
+ var graphClient = this.ServiceProvider?.GetRequiredService();
+ var user = await graphClient.Me.GetAsync();
+ var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has changed");
+ await graphClient?.Me.SendMail.PostAsync(graphEmailMessage);
+ }
+ catch (Exception e)
+ {
+ throw new KernelException($"Something went wrong and couldn't send email - {e}");
+ }
+ }
+ }
+ ```
+ 4. Link the `KernelProcessEventsSubscriber` based class (example: `CounterProcessSubscriber`) to the process builder.
+ ```C#
+ processBuilder.LinkEventSubscribersFromType(serviceProvider);
+ ```
+
+### Setup
+
+#### Microsoft Graph Setup
+
+##### Create an App Registration in Azure Active Directory
+
+1. Go to the [Azure Portal](https://portal.azure.com/).
+2. Select the Azure Active Directory service.
+3. Select App registrations and click on New registration.
+4. Fill in the required fields and click on Register.
+5. Copy the Application **(client) Id** for later use.
+6. Save Directory **(tenant) Id** for later use..
+7. Click on Certificates & secrets and create a new client secret. (Any name and expiration date will work)
+8. Copy the **client secret** value for later use.
+9. Click on API permissions and add the following permissions:
+ - Microsoft Graph
+ - Delegated permissions
+ - OpenId permissions
+ - email
+ - profile
+ - openid
+ - User.Read
+ - Mail.Send (Necessary for sending emails from your account)
+
+##### Set Secrets using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
+
+```powershell
+dotnet user-secrets set "AzureEntraId:TenantId" " ... your tenant id ... "
+dotnet user-secrets set "AzureEntraId:ClientId" " ... your client id ... "
+
+# App Registration Authentication
+dotnet user-secrets set "AzureEntraId:ClientSecret" " ... your client secret ... "
+# OR User Authentication (Interactive)
+dotnet user-secrets set "AzureEntraId:InteractiveBrowserAuthentication" "true"
+dotnet user-secrets set "AzureEntraId:RedirectUri" " ... your redirect uri ... "
+```
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json b/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json
new file mode 100644
index 000000000000..b8a90ee3e4dc
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json
@@ -0,0 +1,23 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "MicrosoftGraph.TenantId": "",
+ "MicrosoftGraph.ClientId": ""
+ },
+ "AzureEntraId": {
+ "Instance": "https://login.microsoftonline.com/",
+ "TenantId": "",
+ "ClientId": "",
+ "ClientSecret": "",
+ "InteractiveBrowserAuthentication": true
+ },
+ "MicrosoftGraph": {
+ "BaseUrl": "https://graph.microsoft.com/v1.0"
+ }
+}
diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md
index ff28c1a91a80..b207fae3c2e8 100644
--- a/dotnet/samples/GettingStartedWithProcesses/README.md
+++ b/dotnet/samples/GettingStartedWithProcesses/README.md
@@ -49,6 +49,27 @@ flowchart LR
### Step02_AccountOpening
+The account opening sample has 3 different implementations covering the same scenario, it just uses different SK components to achieve the same goal.
+
+In addition, the sample introduces the concept of using smaller process as steps to maintain the main process readable and manageble for future improvements and unit testing.
+Also introduces the use of SK Event Subscribers.
+
+A process for opening an account for this sample has the following steps:
+- Fill New User Account Application Form
+- Verify Applicant Credit Score
+- Apply Fraud Detection Analysis to the Application Form
+- Create New Entry in Core System Records
+- Add new account to Marketing Records
+- CRM Record Creation
+- Mail user a user a notification about:
+ - Failure to open a new account due to Credit Score Check
+ - Failure to open a new account due to Fraud Detection Alert
+ - Welcome package including new account details
+
+A SK process that only connects the steps listed above as is (no use of subprocesses as steps) for opening an account look like this:
+
+#### Step02a_AccountOpening
+
```mermaid
flowchart LR
User(User) -->|Provides user details| FillForm(Fill New
Customer
Form)
@@ -79,6 +100,121 @@ flowchart LR
Mailer -->|End of Interaction| User
```
+#### Step02b_AccountOpening
+
+After grouping steps that have a common theme/dependencies, and creating smaller subprocesses and using them as steps,
+the root process looks like this:
+
+```mermaid
+flowchart LR
+ User(User)
+ FillForm(Chat With User
to Fill New
Customer Form)
+ NewAccountVerification[[New Account Verification
Process]]
+ NewAccountCreation[[New Account Creation
Process]]
+ Mailer(Mail
Service)
+
+ User<-->|Provides user details|FillForm
+ FillForm-->|New User Form|NewAccountVerification
+ NewAccountVerification-->|Account Verification
Failed|Mailer
+ NewAccountVerification-->|Account Verification
Succeded|NewAccountCreation
+ NewAccountCreation-->|Account Creation
Succeded|Mailer
+```
+
+Where processes used as steps, which are reusing the same steps used [`Step02a_AccountOpening`](#step02a_accountopening), are:
+
+```mermaid
+graph LR
+ NewUserForm([New User Form])
+ NewUserFormConv([Form Filling Interaction])
+
+ subgraph AccountCreation[Account Creation Process]
+ direction LR
+ AccountValidation([Account Verification Passed])
+ NewUser1([New User Form])
+ NewUserFormConv1([Form Filling Interaction])
+
+ CoreSystem(Core System
Record
Creation)
+ Marketing(New Marketing
Record Creation)
+ CRM(CRM Record
Creation)
+ Welcome(Welcome
Packet)
+ NewAccountCreation([New Account Success])
+
+ NewUser1-->CoreSystem
+ NewUserFormConv1-->CoreSystem
+
+ AccountValidation-->CoreSystem
+ CoreSystem-->CRM-->|Success|Welcome
+ CoreSystem-->Marketing-->|Success|Welcome
+ CoreSystem-->|Account Details|Welcome
+
+ Welcome-->NewAccountCreation
+ end
+
+ subgraph AccountVerification[Account Verification Process]
+ direction LR
+ NewUser2([New User Form])
+ CreditScoreCheck[Credit Check
Step]
+ FraudCheck[Fraud Detection
Step]
+ AccountVerificationPass([Account Verification Passed])
+ AccountCreditCheckFail([Credit Check Failed])
+ AccoutFraudCheckFail([Fraud Check Failed])
+
+
+ NewUser2-->CreditScoreCheck-->|Credit Score
Check Passed|FraudCheck
+ FraudCheck-->AccountVerificationPass
+
+ CreditScoreCheck-->AccountCreditCheckFail
+ FraudCheck-->AccoutFraudCheckFail
+ end
+
+ AccountVerificationPass-->AccountValidation
+ NewUserForm-->NewUser1
+ NewUserForm-->NewUser2
+ NewUserFormConv-->NewUserFormConv1
+
+```
+
+#### Step02c_AccountOpeningWithCloudEvents
+
+An additional optimization that could be made to the Account Creation sample, is to make use of SK Event subscriber to isolate logic that has to do with cloud events.
+In this sample, the cloud event logic is mocked by the Mail Service functionality, which mocks sending an email to the user in different circumstances:
+
+- When new user credit score check fails
+- When new user fraud detection fails
+- When a new account was created successfully after passing all checks and creation steps
+
+When using SK Event subscribers, specific process events when trigged will emit the event data externally to
+any subscribers linked to specific events.
+
+```mermaid
+graph LR
+ subgraph EventSubscribers[SK Event Subscribers]
+ OnSendMailDueCreditCheckFailure[OnSendMailDueCredit
CheckFailure]
+ OnSendMailDueFraudCheckFailure[OnSendMailDueFraud
CheckFailure]
+ OnSendMailWithNewAccountInfo[OnSendMailWith
NewAccountInfo]
+ end
+
+ subgraph Process[SK Process]
+ direction LR
+ User(User)
+ FillForm(Chat With User
to Fill New
Customer Form)
+ NewAccountVerification[[New Account Verification
Process]]
+ NewAccountCreation[[New Account Creation
Process]]
+
+ User<-->|Provides user details|FillForm
+ FillForm-->|New User Form|NewAccountVerification-->|Account Verification
Succeded|NewAccountCreation
+ end
+
+ NewAccountVerification-->|Account Credit Check
Failed|OnSendMailDueCreditCheckFailure
+ NewAccountVerification-->|Account Fraud Detection
Failed|OnSendMailDueFraudCheckFailure
+ NewAccountCreation-->|Account Creation
Succeded|OnSendMailWithNewAccountInfo
+
+```
+Creating a separation with SK Process when using cloud events (even though in this sample it's a mock of a Mailer), it is useful since
+it can help to isolate additional logic related to authentication, use of additional frameworks, etc.
+
+For a more realistic sample of SK Process emitting real cloud events check out the [`ProcessWithCloudEvents` Demo](../Demos/ProcessWithCloudEvents/README.md).
+
### Step03a_FoodPreparation
This tutorial contains a set of food recipes associated with the Food Preparation Processes of a restaurant.
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs
index 6f732669d5dc..0e8274fe6900 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs
@@ -4,7 +4,7 @@ namespace Step02.Models;
///
/// Represents the data structure for a form capturing details of a new customer, including personal information, contact details, account id and account type.
-/// Class used in samples
+/// Class used in samples
///
public class AccountDetails : NewCustomerForm
{
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs
index de1110854e27..32bcd0cca4d9 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs
@@ -3,7 +3,7 @@ namespace Step02.Models;
///
/// Processes Events related to Account Opening scenarios.
-/// Class used in samples
+/// Class used in samples
///
public static class AccountOpeningEvents
{
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs
index 123f0b2e417d..0db9a7987fa1 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs
@@ -7,7 +7,7 @@ namespace Step02.Models;
///
/// Represents the details of interactions between a user and service, including a unique identifier for the account,
/// a transcript of conversation with the user, and the type of user interaction.
-/// Class used in samples
+/// Class used in samples
///
public record AccountUserInteractionDetails
{
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs
index 057e97c81597..fd10646a8b74 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs
@@ -4,7 +4,7 @@ namespace Step02.Models;
///
/// Holds details for a new entry in a marketing database, including the account identifier, contact name, phone number, and email address.
-/// Class used in samples
+/// Class used in samples
///
public record MarketingNewEntryDetails
{
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs
index c000b8491d24..1d469b19d994 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs
@@ -7,7 +7,7 @@ namespace Step02.Models;
///
/// Represents the data structure for a form capturing details of a new customer, including personal information and contact details.
-/// Class used in samples
+/// Class used in samples
///
public class NewCustomerForm
{
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs
similarity index 99%
rename from dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs
rename to dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs
index a523dc4119a3..2c033dfad8e0 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs
@@ -14,7 +14,7 @@ namespace Step02;
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
/// For visual reference of the process check the diagram .
///
-public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
+public class Step02a_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
{
// Target Open AI Services
protected override bool ForceOpenAI => true;
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
index 3b72a9aff192..743851e7b14d 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs
@@ -9,13 +9,15 @@ namespace Microsoft.SemanticKernel;
///
/// A serializable representation of a Process.
///
-public sealed record KernelProcess : KernelProcessStepInfo
+public sealed record KernelProcess : KernelProcessStepInfo // TODO: Should be renamed to KernelProcessInfo to keep consistent names
{
///
/// The collection of Steps in the Process.
///
public IList Steps { get; }
+ public KernelProcessEventsSubscriberInfo? EventsSubscriber { get; set; } = null;
+
///
/// Captures Kernel Process State into after process has run
///
@@ -31,12 +33,14 @@ public KernelProcessStateMetadata ToProcessStateMetadata()
/// The process state.
/// The steps of the process.
/// The edges of the process.
- public KernelProcess(KernelProcessState state, IList steps, Dictionary>? edges = null)
+ /// TODO: may need to reorder params
+ public KernelProcess(KernelProcessState state, IList steps, Dictionary>? edges = null, KernelProcessEventsSubscriberInfo? eventsSubscriber = null)
: base(typeof(KernelProcess), state, edges ?? [])
{
Verify.NotNull(steps);
Verify.NotNullOrWhiteSpace(state.Name);
this.Steps = [.. steps];
+ this.EventsSubscriber = eventsSubscriber;
}
}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs
index 224d5b67bb56..71b86dd2b472 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs
@@ -17,6 +17,12 @@ public sealed class KernelProcessEdge
[DataMember]
public string SourceStepId { get; init; }
+ [DataMember]
+ public string SourceEventName { get; init; }
+
+ [DataMember]
+ public string SourceEventId { get; init; }
+
///
/// The collection of s that are the output of the source Step.
///
@@ -26,12 +32,16 @@ public sealed class KernelProcessEdge
///
/// Creates a new instance of the class.
///
- public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTarget)
+ public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTarget, string sourceEventName, string sourceEventId)
{
Verify.NotNullOrWhiteSpace(sourceStepId);
+ Verify.NotNullOrWhiteSpace(sourceEventId);
+ Verify.NotNullOrWhiteSpace(sourceEventName);
Verify.NotNull(outputTarget);
this.SourceStepId = sourceStepId;
+ this.SourceEventId = sourceEventId;
+ this.SourceEventName = sourceEventName;
this.OutputTarget = outputTarget;
}
}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs
new file mode 100644
index 000000000000..acc60431cf23
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel.Process;
+
+public class KernelProcessEventsSubscriber
+{
+ public IServiceProvider? ServiceProvider { get; init; }
+
+ protected KernelProcessEventsSubscriber() { }
+}
+
+///
+/// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process
+///
+/// Enum that contains all process events that could be subscribed to
+public class KernelProcessEventsSubscriber : KernelProcessEventsSubscriber where TEvents : Enum
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public KernelProcessEventsSubscriber() { }
+
+ ///
+ /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class ProcessEventSubscriberAttribute : Attribute
+ {
+ ///
+ /// Gets the enum of the event that the function is linked to
+ ///
+ public TEvents EventEnum { get; }
+
+ ///
+ /// Gets the string of the event name that the function is linked to
+ ///
+ public string EventName { get; }
+
+ ///
+ /// Initializes the attribute.
+ ///
+ /// Specific Process Event enum
+ public ProcessEventSubscriberAttribute(TEvents eventEnum)
+ {
+ this.EventEnum = eventEnum;
+ this.EventName = Enum.GetName(typeof(TEvents), eventEnum) ?? "";
+ }
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs
new file mode 100644
index 000000000000..bc60385721d2
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs
@@ -0,0 +1,104 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.SemanticKernel.Process;
+
+namespace Microsoft.SemanticKernel;
+
+public class KernelProcessEventsSubscriberInfo
+{
+ private readonly Dictionary> _eventHandlers = [];
+ private readonly Dictionary _stepEventProcessEventMap = [];
+
+ // potentially _processEventSubscriberType, _subscriberServiceProvider, _processEventSubscriber can be converted to a dictionary to support
+ // many unique subscriber classes that could be linked to different ServiceProviders
+ private Type? _processEventSubscriberType = null;
+ private IServiceProvider? _subscriberServiceProvider = null;
+ private KernelProcessEventsSubscriber? _processEventSubscriber = null;
+
+ protected void Subscribe(string eventName, MethodInfo method)
+ {
+ if (this._eventHandlers.TryGetValue(eventName, out List? eventHandlers) && eventHandlers != null)
+ {
+ eventHandlers.Add(method);
+ }
+ }
+
+ public void LinkStepEventToProcessEvent(string stepEventId, string processEventId)
+ {
+ this._stepEventProcessEventMap.Add(stepEventId, processEventId);
+ if (!this._eventHandlers.ContainsKey(processEventId))
+ {
+ this._eventHandlers.Add(processEventId, []);
+ }
+ }
+
+ public void TryInvokeProcessEventFromStepMessage(string stepEventId, object? data)
+ {
+ if (this._stepEventProcessEventMap.TryGetValue(stepEventId, out var processEvent) && processEvent != null)
+ {
+ this.InvokeProcessEvent(processEvent, data);
+ }
+ }
+
+ public void InvokeProcessEvent(string eventName, object? data)
+ {
+ if (this._processEventSubscriberType != null && this._eventHandlers.TryGetValue(eventName, out List? linkedMethods) && linkedMethods != null)
+ {
+ if (this._processEventSubscriber == null)
+ {
+ try
+ {
+ this._processEventSubscriber = (KernelProcessEventsSubscriber?)Activator.CreateInstance(this._processEventSubscriberType, []);
+ this._processEventSubscriberType.GetProperty(nameof(KernelProcessEventsSubscriber.ServiceProvider))?.SetValue(this._processEventSubscriber, this._subscriberServiceProvider);
+ }
+ catch (Exception)
+ {
+ throw new KernelException($"Could not create an instance of {this._processEventSubscriberType.Name} to be used in KernelProcessSubscriberInfo");
+ }
+ }
+
+ foreach (var method in linkedMethods)
+ {
+ method.Invoke(this._processEventSubscriber, [data]);
+ }
+ }
+ }
+
+ ///
+ /// Extracts the event properties and function details of the functions with the annotator
+ ///
+ ///
+ /// Type of the class that make uses of the annotators and contains the functionality to be executed
+ /// Enum that contains the process subscribable events
+ ///
+ public void SubscribeToEventsFromClass(IServiceProvider? serviceProvider = null) where TEventListeners : KernelProcessEventsSubscriber where TEvents : Enum
+ {
+ if (this._subscriberServiceProvider != null)
+ {
+ throw new KernelException("Already linked process to a specific service provider class");
+ }
+
+ var methods = typeof(TEventListeners).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly);
+ foreach (var method in methods)
+ {
+ if (method.GetCustomAttributes(typeof(KernelProcessEventsSubscriber<>.ProcessEventSubscriberAttribute), false).FirstOrDefault() is KernelProcessEventsSubscriber.ProcessEventSubscriberAttribute attribute)
+ {
+ if (attribute.EventEnum.GetType() != typeof(TEvents))
+ {
+ throw new InvalidOperationException($"The event type {attribute.EventEnum.GetType().Name} does not match the expected type {typeof(TEvents).Name}");
+ }
+
+ this.Subscribe(attribute.EventName, method);
+ }
+ }
+
+ this._subscriberServiceProvider = serviceProvider;
+ this._processEventSubscriberType = typeof(TEventListeners);
+ }
+
+ public KernelProcessEventsSubscriberInfo() { }
+}
diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
index e8ed21744da1..6489cc806985 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.SemanticKernel.Process;
using Microsoft.SemanticKernel.Process.Internal;
using Microsoft.SemanticKernel.Process.Models;
@@ -11,7 +12,7 @@ namespace Microsoft.SemanticKernel;
///
/// Provides functionality for incrementally defining a process.
///
-public sealed class ProcessBuilder : ProcessStepBuilder
+public class ProcessBuilder : ProcessStepBuilder
{
/// The collection of steps within this process.
private readonly List _steps = [];
@@ -22,6 +23,8 @@ public sealed class ProcessBuilder : ProcessStepBuilder
/// Maps external input event Ids to the target entry step for the event.
private readonly Dictionary _externalEventTargetMap = [];
+ internal KernelProcessEventsSubscriberInfo _eventsSubscriber;
+
///
/// A boolean indicating if the current process is a step within another process.
///
@@ -108,7 +111,6 @@ internal override KernelProcessStepInfo BuildStep(KernelProcessStepStateMetadata
}
#region Public Interface
-
///
/// A read-only collection of steps in the process.
///
@@ -258,7 +260,7 @@ public KernelProcess Build(KernelProcessStateMetadata? stateMetadata = null)
// Create the process
var state = new KernelProcessState(this.Name, version: this.Version, id: this.HasParentProcess ? this.Id : null);
- var process = new KernelProcess(state, builtSteps, builtEdges);
+ var process = new KernelProcess(state, builtSteps, builtEdges, this._eventsSubscriber);
return process;
}
@@ -269,7 +271,49 @@ public KernelProcess Build(KernelProcessStateMetadata? stateMetadata = null)
public ProcessBuilder(string name)
: base(name)
{
+ this._eventsSubscriber = new();
+ }
+
+ #endregion
+}
+
+public sealed class ProcessBuilder : ProcessBuilder where TEvents : Enum, new()
+{
+ private readonly Dictionary _eventNames = [];
+
+ private void PopulateEventNames()
+ {
+ foreach (TEvents processEvent in Enum.GetValues(typeof(TEvents)))
+ {
+ this._eventNames.Add(processEvent, Enum.GetName(typeof(TEvents), processEvent)!);
+ }
+ }
+
+ #region Public Interface
+
+ public void LinkEventSubscribersFromType(IServiceProvider? serviceProvider = null) where TEventListeners : KernelProcessEventsSubscriber
+ {
+ this._eventsSubscriber.SubscribeToEventsFromClass(serviceProvider);
+ }
+
+ public ProcessEdgeBuilder OnInputEvent(TEvents eventId)
+ {
+ return this.OnInputEvent(this.GetEventName(eventId));
+ }
+
+ public string GetEventName(TEvents processEvent)
+ {
+ return this._eventNames[processEvent];
}
+ public ProcessEdgeBuilder GetProcessEvent(TEvents processEvent)
+ {
+ return this.OnInputEvent(this.GetEventName(processEvent));
+ }
+
+ public ProcessBuilder(string name) : base(name)
+ {
+ this.PopulateEventNames();
+ }
#endregion
}
diff --git a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
index 076912f318ec..bc6712dcd75a 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
@@ -36,7 +36,7 @@ internal ProcessEdgeBuilder(ProcessBuilder source, string eventId)
public ProcessEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target)
{
this.Target = target;
- ProcessStepEdgeBuilder edgeBuilder = new(this.Source, this.EventId) { Target = this.Target };
+ ProcessStepEdgeBuilder edgeBuilder = new(this.Source, this.EventId, this.EventId) { Target = this.Target };
this.Source.LinkTo(this.EventId, edgeBuilder);
return new ProcessEdgeBuilder(this.Source, this.EventId);
diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
index 70749a751da3..be2f697f704d 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
@@ -35,13 +35,12 @@ public abstract class ProcessStepBuilder
///
/// Define the behavior of the step when the event with the specified Id is fired.
///
- /// The Id of the event of interest.
/// An instance of .
- public ProcessStepEdgeBuilder OnEvent(string eventId)
+ public ProcessStepEdgeBuilder OnEvent(string eventName)
{
// scope the event to this instance of this step
- var scopedEventId = this.GetScopedEventId(eventId);
- return new ProcessStepEdgeBuilder(this, scopedEventId);
+ var scopedEventId = this.GetScopedEventId(eventName);
+ return new ProcessStepEdgeBuilder(this, scopedEventId, eventName);
}
///
diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs
index 2e4afbfa51e9..5aafadc096c0 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs
@@ -14,9 +14,15 @@ public sealed class ProcessStepEdgeBuilder
///
/// The event Id that the edge fires on.
+ /// Unique event Id linked to the source id.
///
internal string EventId { get; }
+ ///
+ /// The event name that the edge fires on.
+ ///
+ internal string EventName { get; }
+
///
/// The source step of the edge.
///
@@ -27,13 +33,16 @@ public sealed class ProcessStepEdgeBuilder
///
/// The source step.
/// The Id of the event.
- internal ProcessStepEdgeBuilder(ProcessStepBuilder source, string eventId)
+ /// The name of the event
+ internal ProcessStepEdgeBuilder(ProcessStepBuilder source, string eventId, string eventName)
{
Verify.NotNull(source);
Verify.NotNullOrWhiteSpace(eventId);
+ Verify.NotNullOrWhiteSpace(eventName);
this.Source = source;
this.EventId = eventId;
+ this.EventName = eventName;
}
///
@@ -44,7 +53,7 @@ internal KernelProcessEdge Build()
Verify.NotNull(this.Source?.Id);
Verify.NotNull(this.Target);
- return new KernelProcessEdge(this.Source.Id, this.Target.Build());
+ return new KernelProcessEdge(this.Source.Id, this.Target.Build(), this.EventName, this.EventId);
}
///
@@ -62,7 +71,20 @@ public ProcessStepEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target)
this.Target = target;
this.Source.LinkTo(this.EventId, this);
- return new ProcessStepEdgeBuilder(this.Source, this.EventId);
+ return new ProcessStepEdgeBuilder(this.Source, this.EventId, this.EventName);
+ }
+
+ ///
+ /// Forward specific step events to process events so specific functions linked get executed
+ /// when receiving the specific event
+ ///
+ ///
+ ///
+ public ProcessStepEdgeBuilder EmitAsProcessEvent(ProcessEdgeBuilder processEdge)
+ {
+ processEdge.Source._eventsSubscriber?.LinkStepEventToProcessEvent(this.EventId, processEventId: processEdge.EventId);
+
+ return new ProcessStepEdgeBuilder(this.Source, this.EventId, this.EventName);
}
///
diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs
index 7b4f239f8965..4234a727710a 100644
--- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs
+++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs
@@ -256,6 +256,9 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep
List messageTasks = [];
foreach (var message in messagesToProcess)
{
+ // Check if message has external event handler linked to it
+ this.TryEmitMessageToExternalSubscribers(message);
+
// Check for end condition
if (message.DestinationId.Equals(ProcessConstants.EndStepName, StringComparison.OrdinalIgnoreCase))
{
@@ -291,6 +294,16 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep
return;
}
+ private void TryEmitMessageToExternalSubscribers(string processEventId, object? processEventData)
+ {
+ this._process.EventsSubscriber?.TryInvokeProcessEventFromStepMessage(processEventId, processEventData);
+ }
+
+ private void TryEmitMessageToExternalSubscribers(ProcessMessage message)
+ {
+ this.TryEmitMessageToExternalSubscribers(message.EventId, message.TargetEventData);
+ }
+
///
/// Processes external events that have been sent to the process, translates them to s, and enqueues
/// them to the provided message channel so that they can be processed in the next superstep.
@@ -338,9 +351,9 @@ private void EnqueueStepMessages(LocalStep step, Queue messageCh
}
// Error event was raised with no edge to handle it, send it to an edge defined as the global error target.
- if (!foundEdge && stepEvent.IsError)
+ if (!foundEdge)
{
- if (this._outputEdges.TryGetValue(ProcessConstants.GlobalErrorEventId, out List? edges))
+ if (stepEvent.IsError && this._outputEdges.TryGetValue(ProcessConstants.GlobalErrorEventId, out List? edges))
{
foreach (KernelProcessEdge edge in edges)
{
@@ -348,6 +361,11 @@ private void EnqueueStepMessages(LocalStep step, Queue messageCh
messageChannel.Enqueue(message);
}
}
+ else
+ {
+ // Checking in case the step with no edges linked to it has event that should be emitted externally
+ this.TryEmitMessageToExternalSubscribers(stepEvent.QualifiedId, stepEvent.Data);
+ }
}
}
}
diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs
index f3de5b7cfa32..494a468efd44 100644
--- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs
+++ b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs
@@ -127,7 +127,7 @@ private static void VerifyContainerSerialization(ProcessMessage[] processMessage
private static ProcessMessage CreateMessage(Dictionary values)
{
- return new ProcessMessage("test-source", "test-destination", "test-function", values)
+ return new ProcessMessage("test-event", "test-eventid", "test-source", "test-destination", "test-function", values)
{
TargetEventData = "testdata",
TargetEventId = "targetevent",
diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs
index 07c4802c8731..0d5e085f2ac7 100644
--- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs
+++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs
@@ -91,7 +91,7 @@ public void LinkToShouldAddEdge()
{
// Arrange
var stepBuilder = new TestProcessStepBuilder("TestStep");
- var edgeBuilder = new ProcessStepEdgeBuilder(stepBuilder, "TestEvent");
+ var edgeBuilder = new ProcessStepEdgeBuilder(stepBuilder, "TestEvent", "TestEvent");
// Act
stepBuilder.LinkTo("TestEvent", edgeBuilder);
diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs
index 3e3f128e1753..35d81ef1ca97 100644
--- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs
+++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs
@@ -21,7 +21,7 @@ public void ConstructorShouldInitializeProperties()
var eventType = "Event1";
// Act
- var builder = new ProcessStepEdgeBuilder(source, eventType);
+ var builder = new ProcessStepEdgeBuilder(source, eventType, eventType);
// Assert
Assert.Equal(source, builder.Source);
@@ -36,7 +36,7 @@ public void SendEventToShouldSetOutputTarget()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
var outputTarget = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("OutputStep"));
// Act
@@ -54,7 +54,7 @@ public void SendEventToShouldSetMultipleOutputTargets()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
var outputTargetA = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("StepA"));
var outputTargetB = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("StepB"));
@@ -75,7 +75,7 @@ public void SendEventToShouldThrowIfOutputTargetAlreadySet()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
var outputTarget1 = new ProcessFunctionTargetBuilder(source);
var outputTarget2 = new ProcessFunctionTargetBuilder(source);
@@ -94,7 +94,7 @@ public void StopProcessShouldSetOutputTargetToEndStep()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
// Act
builder.StopProcess();
@@ -111,7 +111,7 @@ public void StopProcessShouldThrowIfOutputTargetAlreadySet()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
var outputTarget = new ProcessFunctionTargetBuilder(source);
// Act
@@ -129,7 +129,7 @@ public void BuildShouldReturnKernelProcessEdge()
{
// Arrange
var source = new ProcessStepBuilder(TestStep.Name);
- var builder = new ProcessStepEdgeBuilder(source, "Event1");
+ var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1");
var outputTarget = new ProcessFunctionTargetBuilder(source);
builder.SendEventTo(outputTarget);
diff --git a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs
index e1f8957038cd..f615a2d2c4e8 100644
--- a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs
+++ b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs
@@ -139,16 +139,18 @@ private static void VerifyProcess(KernelProcess expected, KernelProcess actual)
}
}
- private static Dictionary> CreateTestEdges() =>
- new()
+ private static Dictionary> CreateTestEdges()
+ {
+ return new()
{
{
"sourceId",
[
- new KernelProcessEdge("sourceId", new KernelProcessFunctionTarget("sourceId", "targetFunction", "targetParameter", "targetEventId")),
+ new KernelProcessEdge("sourceId", new KernelProcessFunctionTarget("sourceId", "targetFunction", "targetParameter", "targetEventId"), "eventName", "eventId"),
]
}
};
+ }
private sealed record TestState
{
diff --git a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs
index 6b7a73c57a15..92b15a5495f3 100644
--- a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs
+++ b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs
@@ -10,12 +10,16 @@ namespace Microsoft.SemanticKernel.Process.Runtime;
///
/// Initializes a new instance of the class.
///
+/// Original name of the name of the event triggered
+/// Original name of the name of the event triggered
/// The source identifier of the message.
/// The destination identifier of the message.
/// The name of the function associated with the message.
/// The dictionary of values associated with the message.
[KnownType(typeof(KernelProcessError))]
public record ProcessMessage(
+ string EventName,
+ string EventId,
string SourceId,
string DestinationId,
string FunctionName,
diff --git a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs
index f1bcea825c22..2b706e8b39bf 100644
--- a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs
+++ b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs
@@ -24,7 +24,7 @@ internal static ProcessMessage CreateFromEdge(KernelProcessEdge edge, object? da
parameterValue.Add(target.ParameterName!, data);
}
- ProcessMessage newMessage = new(edge.SourceStepId, target.StepId, target.FunctionName, parameterValue)
+ ProcessMessage newMessage = new(edge.SourceEventName, edge.SourceEventId, edge.SourceStepId, target.StepId, target.FunctionName, parameterValue)
{
TargetEventId = target.TargetEventId,
TargetEventData = data