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

  1. Make the generic type argument match the type actually passed to TriggerSignal for that signal name.
  2. Inspect what the workflow signals (search for TriggerSignal calls) and align the test's WaitAsync<T> type parameter with it.
  3. Use the non-generic WaitAsync(signal) and log/inspect result.GetType() to discover the actual payload type.
  4. 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

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


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)