diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs
index e25afabc6..44bf24570 100644
--- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs
+++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs
@@ -1,3 +1,4 @@
+using Elsa.Common.Contracts;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
@@ -109,4 +110,36 @@ public static class WorkflowExecutionContextExtensions
/// Returns true if all activities have completed or canceled, false otherwise.
///
public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.ActiveActivityExecutionContexts.All(x => x.Status != ActivityStatus.Running);
+
+ ///
+ /// Adds a new to the execution log of the current .
+ ///
+ /// The being extended.
+ /// The name of the event.
+ /// The message of the event.
+ /// Any contextual data related to this event.
+ /// Returns the created .
+ public static WorkflowExecutionLogEntry AddExecutionLogEntry(this WorkflowExecutionContext context, string eventName, string? message = default, object? payload = default)
+ {
+ var now = context.GetRequiredService().UtcNow;
+
+ var logEntry = new WorkflowExecutionLogEntry(
+ context.Id,
+ default,
+ context.Workflow.Id,
+ context.Workflow.Type,
+ context.Workflow.Identity.Version,
+ context.Workflow.Name,
+ context.Workflow.Identity.Id,
+ default,
+ now,
+ context.ExecutionLogSequence++,
+ eventName,
+ message,
+ context.Workflow.GetSource(),
+ payload);
+
+ context.ExecutionLog.Add(logEntry);
+ return logEntry;
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
index eb6b40b3b..9b107d846 100644
--- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
@@ -47,7 +47,9 @@ public class WorkflowsFeature : FeatureBase
///
/// A delegate to configure the .
///
- public Action WorkflowExecutionPipeline { get; set; } = builder => builder.UseDefaultActivityScheduler();
+ public Action WorkflowExecutionPipeline { get; set; } = builder => builder
+ .UseExceptionHandling()
+ .UseDefaultActivityScheduler();
///
/// A delegate to configure the .
diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs
new file mode 100644
index 000000000..527c0531f
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs
@@ -0,0 +1,60 @@
+using Elsa.Common.Contracts;
+using Elsa.Extensions;
+using Elsa.Workflows.Core.Contracts;
+using Elsa.Workflows.Core.Models;
+using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
+using Elsa.Workflows.Core.State;
+using Microsoft.Extensions.Logging;
+
+namespace Elsa.Workflows.Core.Middleware.Workflows;
+
+///
+/// Adds extension methods to .
+///
+public static class ExceptionHandlingMiddlewareExtensions
+{
+ ///
+ /// Installs the component in the activity execution pipeline.
+ ///
+ public static IWorkflowExecutionPipelineBuilder UseExceptionHandling(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware();
+}
+
+///
+/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state.
+///
+public class ExceptionHandlingMiddleware : IWorkflowExecutionMiddleware
+{
+ private readonly WorkflowMiddlewareDelegate _next;
+ private readonly ISystemClock _systemClock;
+ private readonly ILogger _logger;
+
+ ///
+ /// Constructor.
+ ///
+ public ExceptionHandlingMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, ILogger logger)
+ {
+ _next = next;
+ _systemClock = systemClock;
+ _logger = logger;
+ }
+
+ ///
+ public async ValueTask InvokeAsync(WorkflowExecutionContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning(e, "An exception was caught from a downstream middleware component");
+ var exceptionState = ExceptionState.FromException(e);
+ var now = _systemClock.UtcNow;
+ var activity = context.Workflow;
+ var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now);
+ context.Incidents.Add(incident);
+ context.TransitionTo(WorkflowSubStatus.Faulted);
+ context.AddExecutionLogEntry("Faulted", e.Message, exceptionState);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs
deleted file mode 100644
index b50c2c996..000000000
--- a/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Elsa.Workflows.Core.Models;
-
-///
-/// Holds information about a workflow fault.
-///
-/// The exception that occurred
-/// A description about the fault. Usually the exception message, if there waa an exception.
-/// The ID of the activity that caused the workflow to fault.
-public record WorkflowFault(Exception? Exception, string Message, string? FaultedActivityId);
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs
index b4cdce239..d09bd772d 100644
--- a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs
+++ b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs
@@ -33,5 +33,7 @@ public class WorkflowExecutionPipeline : IWorkflowExecutionPipeline
///
public async Task ExecuteAsync(WorkflowExecutionContext context) => await Pipeline(context);
- private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseDefaultActivityScheduler());
+ private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x
+ .UseExceptionHandling()
+ .UseDefaultActivityScheduler());
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs
index 2d828668d..f10418638 100644
--- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs
@@ -21,6 +21,7 @@ public static class WorkflowExecutionPipelineBuilderExtensions
.UseBookmarkPersistence()
.UseActivityExecutionLogPersistence()
.UseWorkflowExecutionLogPersistence()
+ .UseExceptionHandling()
.UseDefaultActivityScheduler();
///