elsa-workflows/elsa-core · error · ArgumentException

User Task action keys must be unique.

Error message

User Task action keys must be unique.

What it means

Normalize() enforces that all User Task action keys are unique, compared case-insensitively (OrdinalIgnoreCase). Duplicate keys would make it ambiguous which action the end user selected, so the definition is rejected with an ArgumentException. Because comparison ignores case, 'Approve' and 'approve' also count as duplicates.

Solutions

  1. Deduplicate the Actions collection by key (OrdinalIgnoreCase) before normalizing.
  2. Rename one of the colliding actions and update invitations' AllowedActions accordingly.
  3. Add a distinct-count pre-check (as the code does) in your own validation to fail early with a clear message.

Example fix

// before
Actions = [ new() { Key = "Approve", Label = "Approve" }, new() { Key = "approve", Label = "OK" } ]
// after
Actions = [ new() { Key = "Approve", Label = "Approve" }, new() { Key = "Ok", Label = "OK" } ]
Defensive patterns

Strategy: validation

Validate before calling

if (definition.Actions.Select(a => a.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != definition.Actions.Count)
    throw new InvalidOperationException("Duplicate User Task action keys.");

Try / catch

try { definition = definition.Normalize(); }
catch (ArgumentException ex) when (ex.Message.Contains("must be unique")) { /* show which keys collided */ }

Prevention

When it happens

Trigger: Calling Normalize() on a UserTaskDefinition whose Actions contain two entries with keys differing only in casing, e.g. [{key:'Approve'},{key:'approve'}], or exact duplicates copied from a template.

Common situations: Merging action lists from multiple sources (defaults + user additions); case-insensitive UIs that allowed both 'Approve' and 'approve'; copy/paste of action rows.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/83023b876091a8e6. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.UserTasks/Models/UserTaskModels.cs:213

    public IReadOnlyCollection<UserTaskAction> Actions { get; init; } = [];
    public IReadOnlyCollection<UserTaskInvitationDefinition> Invitations { get; init; } = [];
    public bool EnableTimeoutOutcome { get; init; }
    public bool EnableCancellationOutcome { get; init; }

    public UserTaskDefinitionSnapshot Normalize()
    {
        var actions = Actions.Count == 0
            ? [new UserTaskAction("Complete", "Complete")]
            : Actions;

        if (string.IsNullOrWhiteSpace(Title))
            throw new ArgumentException("A User Task title is required.", nameof(Title));
        if (actions.Any(x => string.IsNullOrWhiteSpace(x.Key) || string.IsNullOrWhiteSpace(x.Label)))
            throw new ArgumentException("User Task action keys and labels are required.", nameof(Actions));
        if (actions.Any(x => string.Equals(x.Key, "Timeout", StringComparison.OrdinalIgnoreCase) || string.Equals(x.Key, "Cancelled", StringComparison.OrdinalIgnoreCase)))
            throw new ArgumentException("Timeout and Cancelled are reserved User Task action keys.");
        if (actions.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != actions.Count)
            throw new ArgumentException("User Task action keys must be unique.");
        if (Priority is < 0 or > 100)
            throw new ArgumentOutOfRangeException(nameof(Priority), "Priority must be between 0 and 100.");
        if (Invitations.Any(x => string.IsNullOrWhiteSpace(x.VerifierName) || x.AllowedActions.Count == 0 || x.AllowedActions.Any(string.IsNullOrWhiteSpace)))
            throw new ArgumentException("Invitation verifier names and allowed actions are required.", nameof(Invitations));
        if (Invitations.Any(invitation => invitation.AllowedActions.Any(allowed => !actions.Any(action => string.Equals(action.Key, allowed, StringComparison.OrdinalIgnoreCase)))))
            throw new ArgumentException("Invitation actions must be configured User Task actions.", nameof(Invitations));

        return this with { Actions = actions };
    }
}

public sealed class UserTask
{
    public string Id { get; set; } = Guid.NewGuid().ToString("N");
    public string TenantId { get; set; } = "";
    public string WorkflowDefinitionId { get; set; } = "";
    public string? WorkflowDefinitionName { get; set; }
    public int? WorkflowDefinitionVersion { get; set; }

View on GitHub (pinned to fe9217bdfa)