diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
index a1bb1883d..144497879 100644
--- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
+++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs
@@ -701,6 +701,13 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
/// True if the memory block exists, false otherwise.
public bool TryGet(MemoryBlockReference blockReference, out object? value)
{
+ // Handle Literal references directly - they hold their value and don't need to be in the memory register
+ if (blockReference is Literal literal)
+ {
+ value = literal.Value;
+ return true;
+ }
+
var memoryBlock = GetMemoryBlock(blockReference);
if (memoryBlock != null)
diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs
index 57b5fae04..56509f366 100644
--- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs
+++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs
@@ -639,6 +639,14 @@ public partial class WorkflowExecutionContext : IExecutionContext
{
RemoveActivityExecutionContexts(x => x is { IsCompleted: true, ParentActivityExecutionContext: not null });
}
+
+ ///
+ /// Clears all activity completion callback entries from the workflow execution context.
+ ///
+ public void ClearCompletionCallbacks()
+ {
+ _completionCallbackEntries.Clear();
+ }
public IEnumerable GetActiveActivityExecutionContexts()
{
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs
new file mode 100644
index 000000000..4570790f6
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Core/LiteralInputTests.cs
@@ -0,0 +1,119 @@
+using Elsa.Expressions.Models;
+using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.Memory;
+using Elsa.Workflows.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace Elsa.Workflows.IntegrationTests.Core;
+
+///
+/// Tests for ActivityExecutionContext handling of Literal inputs in real scenarios
+///
+public class LiteralInputTests
+{
+ private readonly IServiceProvider _services;
+ private readonly IWorkflowRunner _workflowRunner;
+
+ public LiteralInputTests(ITestOutputHelper testOutputHelper)
+ {
+ _services = new TestApplicationBuilder(testOutputHelper).Build();
+ _workflowRunner = _services.GetRequiredService();
+ }
+
+ [Fact(DisplayName = "Activity should be able to access Input created with Literal")]
+ public async Task ActivityShouldAccessLiteralInput()
+ {
+ // Arrange - Create a custom activity that uses another activity with literal input
+ await _services.PopulateRegistriesAsync();
+
+ var workflow = new TestWorkflow(builder =>
+ {
+ builder.Root = new CompositeActivityWithLiteralInput();
+ });
+
+ // Act & Assert - Should not throw
+ var result = await _workflowRunner.RunAsync(workflow);
+ Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
+ }
+
+ [Fact(DisplayName = "ActivityExecutionContext.TryGet should return true and value for Literal")]
+ public async Task TryGet_ShouldHandleLiteralDirectly()
+ {
+ // Arrange - Create a workflow and activity execution context
+ await _services.PopulateRegistriesAsync();
+
+ var activity = new WriteLine("Test");
+ var workflow = new Workflow { Root = activity };
+
+ var workflowGraphBuilder = _services.GetRequiredService();
+ var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
+ var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_services, workflowGraph, "test");
+ var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity);
+
+ // Create a Literal and use it as a MemoryBlockReference
+ var expectedValue = "Hello World";
+ var literal = new Literal(expectedValue);
+ var blockReference = (MemoryBlockReference)literal;
+
+ // Act - Call TryGet directly with the Literal
+ var success = activityExecutionContext.TryGet(blockReference, out var actualValue);
+
+ // Assert - Should succeed and return the literal's value
+ Assert.True(success, "TryGet should return true for Literal references");
+ Assert.Equal(expectedValue, actualValue);
+ }
+
+ [Fact(DisplayName = "ActivityExecutionContext.Get with Input containing Literal should work")]
+ public async Task Get_ShouldWorkWithInputContainingLiteral()
+ {
+ // Arrange
+ await _services.PopulateRegistriesAsync();
+
+ var activity = new WriteLine("Test");
+ var workflow = new Workflow { Root = activity };
+
+ var workflowGraphBuilder = _services.GetRequiredService();
+ var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
+ var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_services, workflowGraph, "test");
+ var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity);
+
+ // Create an Input with a Literal value
+ var expectedValue = 42;
+ var literal = new Literal(expectedValue);
+ var input = new Input(literal);
+
+ // Act - Get the value through the Input (which internally uses TryGet)
+ var actualValue = activityExecutionContext.Get(input);
+
+ // Assert
+ Assert.Equal(expectedValue, actualValue);
+ }
+}
+
+///
+/// A composite activity that creates inputs with literal values and tries to read them
+/// This simulates the use case described in the issue where activities re-use other activities' execute methods
+///
+public class CompositeActivityWithLiteralInput : CodeActivity
+{
+ protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ // Create an Input with a Literal value - this is a common pattern when programmatically
+ // creating activities and setting their inputs
+ var literal = new Literal("Test Value");
+ var input = new Input(literal);
+
+ // Try to get the value - this should work but will fail without the Literal handling in TryGet
+ // The issue is that when Input is created with a Literal, the Literal becomes the MemoryBlockReference
+ // When Get is called, it tries to find this in the memory register, but Literals hold values directly
+ var value = context.Get(input);
+
+ // If we got here without exception, the test passes
+ if (value != "Test Value")
+ throw new Exception($"Expected 'Test Value' but got '{value}'");
+
+ return ValueTask.CompletedTask;
+ }
+}