elsa-workflows/elsa-core · error · InvalidCastException
Signal ' ' was not of type ' '.
Error message
Signal '{signal}' was not of type '{typeof(T).Name}'. What it means
Elsa.Testing.Shared's SignalManager stores signals as objects; the generic WaitAsync<T> retrieves the pending signal and throws InvalidCastException when the stored value cannot be cast to T. In test code this means the workflow or component signaled with a different payload type than the test awaited. The non-generic value is discarded and the test fails with this message.
Solutions
- Make the generic type argument match the type actually passed to TriggerSignal for that signal name.
- Inspect what the workflow signals (search for TriggerSignal calls) and align the test's WaitAsync<T> type parameter with it.
- Use the non-generic WaitAsync(signal) and log/inspect result.GetType() to discover the actual payload type.
- Rename the signal so different payload types don't share one signal key.
Example fix
// before
var payload = await signals.WaitAsync<MyEvent>("done"); // workflow signaled a string
// after
var payload = await signals.WaitAsync<string>("done"); // matches actual signal type Defensive patterns
Strategy: type-guard
Validate before calling
// verify the expected signal type before asserting
var raw = await signals.WaitAsync("done");
if (raw is not MyEvent)
throw new InvalidOperationException($"Signal 'done' produced {raw?.GetType().Name ?? "null"}, expected MyEvent"); Type guard
bool IsValidSignal(object? signal) => signal is MyEvent;
var result = await signals.WaitAsync("done");
if (!IsValidSignal(result))
Assert.Fail($"Unexpected signal payload: {result?.GetType().Name}"); Try / catch
try
{
var payload = await signals.WaitAsync<MyEvent>("done");
}
catch (InvalidCastException ex)
{
Assert.Fail($"Signal payload type mismatch: {ex.Message}");
} Prevention
- Keep signal names and payload types in one shared constants/type file used by both workflow and tests.
- Rename signals whenever their payload type changes.
- In tests, first use the non-generic WaitAsync to inspect the payload type when a cast fails.
When it happens
Trigger: Calling signalManager.WaitAsync<MyPayload>("my-signal") after the workflow triggered the signal with a different payload type (e.g. recorded a string but the test waits for a custom record, or null was signaled); two test threads using the same signal name with different payload types.
Common situations: Refactoring a workflow's signal payload type without updating the awaiting test; reusing a signal name across workflow versions with different payload shapes; a race where the timeout path or a default value was signaled instead of the expected typed payload.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Signal ' ' timed out after milliseconds.
- Workflow definition with ID
- Build() must be called before accessing services
- Cannot convert null to non-nullable type
- Unable to convert value of type
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/7448231ad16c7408.
Report an issue: GitHub.
Appendix: source
Thrown at src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs:14
using System.Collections.Concurrent;
namespace Elsa.Testing.Shared.Services;
public class SignalManager
{
private readonly ConcurrentDictionary<object, TaskCompletionSource<object?>> _signals = new();
public async Task<T> WaitAsync<T>(object signal, int millisecondsTimeout = 60000)
{
var result = await WaitAsync(signal, millisecondsTimeout);
if(result is not T typedResult)
throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'.");
return typedResult;
}
public async Task<object?> WaitAsync(object signal, int millisecondsTimeout = 60000)
{
var taskCompletionSource = GetOrCreate(signal);
using var cancellationTokenSource = new CancellationTokenSource(millisecondsTimeout);
var delayTask = Task.Delay(millisecondsTimeout, cancellationTokenSource.Token);
var completedTask = await Task.WhenAny(taskCompletionSource.Task, delayTask);
if (completedTask == delayTask)
{
_signals.TryRemove(signal, out _);
throw new TimeoutException($"Signal '{signal}' timed out after {millisecondsTimeout} milliseconds.");
}
cancellationTokenSource.Cancel();View on GitHub (pinned to fe9217bdfa)